From 66d5dd5a6274e31c166dcac7308706b7a1dfc859 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Andr=C3=A9=20Wobst?= Date: Mon, 14 Apr 2003 08:47:25 +0000 Subject: [PATCH] pdftex; texter (graph is currently broken); data docstrings --- minor other stuff I've forgotten right now git-svn-id: https://pyx.svn.sourceforge.net/svnroot/pyx/trunk/pyx@913 069f4177-920e-0410-937b-c2a4a81bcd90 --- CHANGES | 10 ++- pyx/data.py | 145 +++++++++++++++++++++++++++++--- pyx/graph.py | 268 ++++++++++++++++++++++++++++++++++++++++++++++++----------- pyx/text.py | 116 ++++++++++++++++---------- 4 files changed, 431 insertions(+), 108 deletions(-) diff --git a/CHANGES b/CHANGES index 7e6973ca..2272e68a 100644 --- a/CHANGES +++ b/CHANGES @@ -5,8 +5,10 @@ PyX - manual: - include sources in source distribution - graph module: + - tick.text is renamed to tick.label (in progress), ticks and labels will become tickpos/tickdist and labelpos/labeldist in partitioning + - separate texter out of the axispainter (in progress) + TODO: - common bboxes for different graphs - - separate texter out of the axispainter - automatic datafile key title - exceptions in drawsymbol & friends... - 3d graphs, circular graphs @@ -17,11 +19,15 @@ PyX - default patterns - stroke and fill should support trafos - make dash length available - - AW asks: shouldn't strokepath/fillpath be validated by "strokepath is not None" instead of just "strokepath" + - shouldn't strokepath/fillpath be validated by "strokepath is not None" instead of just "strokepath" - text module: + - pdf(la)tex support (will hopefully lead to markers in text) + TODO: - further improve lfs handling - tex module: - copy lfs handling from text module (share the same source???) + - data module: + - full documentation via doc strings enhancements: - box module: diff --git a/pyx/data.py b/pyx/data.py index 829218c9..da3cf699 100644 --- a/pyx/data.py +++ b/pyx/data.py @@ -28,31 +28,95 @@ import helper, mathtree class ColumnError(Exception): pass -ColPattern = re.compile(r"\$-?[0-9]+") + +ColPattern = re.compile(r"\$(\(-?[0-9]+\)|-?[0-9]+)") class MathTreeValCol(mathtree.MathTreeValVar): + """column id pattern like "$1" or "$(1)" + defines a new value pattern to identify columns by its number""" + + # __implements__ = ... # TODO: mathtree interfaces def InitByParser(self, arg): Match = arg.MatchPattern(ColPattern) if Match: + # just store the matched string -> handle this variable name later on self.AddArg(Match) return 1 -MathTreeValsWithCol = (mathtree.MathTreeValConst, - mathtree.MathTreeValVar, - MathTreeValCol) +# extent the list of possible values by MathTreeValCol +MathTreeValsWithCol = tuple(list(mathtree.DefaultMathTreeVals) + [MathTreeValCol]) + + +class _Idata: + """interface definition of a data object + data objects store data arranged in rows and columns""" + + titles = [] + """column titles + - a list of strings storing the column titles + - the length of the list must match the number of columns + - any titles entry might be None, thus explicitly not providing a column title""" + + data = [] + """column/row data + - a list of rows where each row represents a data point + - each row contains a list, where each entry of the list represents a value for a column + - the number of columns for each data point must match the number of columns + - any column enty of any data point might be a float, a string, or None""" + + def getcolumnno(self, column): + """returns a column number + - the column parameter might be an integer to be used as a column number + - a column number must be a valid list index (negative values are allowed) + - the column parameter might be a string contained in the titles list; + to be valid, the string must be unique within the titles list + - the method raises ColumnError when the value of the column parameter is invalid""" + + def getcolumn(self, column): + """returns a column + - extracts a column out of self.data and returns it as a list + - the column is identified by the parameter column as in getcolumnno""" + + def addcolumn(self, expression, context={}): + """adds a column defined by a mathematical expression + - evaluates the expression for each data row and adds a new column at + the end of each data row + - the expression must be a valid mathtree expression (see module mathtree) + with an extended variable name syntax: strings like "$i" and "$(i)" are + allowed where i is an integer + - a variable of the mathematical expression might either be a column title + or, by the extended variable name syntax, it defines an integer to be used + as a list index within the column list for each row + - context is a dictionary, where external variables and functions can be + given; those are used in the evaluation of the expression + - when the expression contains the character "=", everything after the last + "=" is interpreted as the mathematical expression while everything before + this character will be used as a column title for the new column; when no + "=" is contained in the expression, the hole expression is taken as the + mathematical expression and the column title is set to None""" class _data: + """an (minimal) implementor of _Idata + other classes providing _Idata might be based on is class""" + + __implements__ = _Idata + def __init__(self, data, titles, parser=mathtree.parser(MathTreeVals=MathTreeValsWithCol)): + """initializes an instance + - data and titles are just set as instance variables without further checks --- + they must be valid in terms of _Idata (expecially their sizes must fit) + - parser is used in addcolumn and thus must implement the expression parsing as + defined in _Idata""" self.data = data self.titles = titles self.parser = parser def getcolumnno(self, column): - if self.titles.count(column) == 1: + if helper.isstring(column) and self.titles.count(column) == 1: return self.titles.index(column) try: self.titles[column] @@ -76,7 +140,10 @@ class _data: columnlist = {} for key in tree.VarList(): if key[0] == "$": - column = int(key[1:]) + if key[1] == "(": + column = int(key[2:-1]) + else: + column = int(key[1:]) try: self.titles[column] except: @@ -89,7 +156,7 @@ class _data: if key not in context.keys(): raise e - varlist = context.copy() + varlist = context.copy() # do not modify context for data in self.data: try: for key in columnlist.keys(): @@ -102,7 +169,25 @@ class _data: class data(_data): + "an implementation of _Idata with an easy to use constructor" + + __implements__ = _Idata + def __init__(self, data=[], titles=[], maxcolumns=helper.nodefault, **kwargs): + """initializes an instance + - data titles must be valid in terms of _Idata except for the number of + columns for each row, especially titles might be the default, e.g. [] + - instead of lists for data, each row in data, and titles, tuples or + any other data structure with sequence like behavior might be used, + but they are converted to lists + - maxcolumns is an integer; when not set, maxcolumns is evaluated out of + the maximum column number in each row of data (not taking into account + the titles list) + - titles and each row in data is extended (or cutted) to fit maxcolumns; + when extending those lists, None entries are appended + - parser is used in addcolumn and thus must implement the expression parsing as + defined in _Idata + - further keyword arguments are passed to the constructor of _data""" if len(data): if maxcolumns is helper.nodefault: maxcolumns = len(data[0]) @@ -121,12 +206,23 @@ class data(_data): class datafile(data): + "an implementation of _Idata reading data from a file" + + __implements__ = _Idata + defaultcommentpattern = re.compile(r"(#+|!+|%+)\s*") defaultstringpattern = re.compile(r"\"(.*?)\"(\s+|$)") defaultcolumnpattern = re.compile(r"(.*?)(\s+|$)") def splitline(self, line, stringpattern, columnpattern, tofloat=1): + """returns a tuple created out of the string line + - matches stringpattern and columnpattern, adds the first group of that + match to the result and and removes those matches until the line is empty + - when stringpattern matched, the result is always kept as a string + - when columnpattern matched and tofloat is true, a conversion to a float + is tried; when this conversion fails, the string is kept""" result = [] + # try to gain speed by skip matching regular expressions if line.find('"')!=-1 or \ stringpattern is not self.defaultstringpattern or \ columnpattern is not self.defaultcolumnpattern: @@ -164,7 +260,23 @@ class datafile(data): def __init__(self, file, commentpattern=defaultcommentpattern, stringpattern=defaultstringpattern, columnpattern=defaultcolumnpattern, - skiphead=0, skiptail=0, every=1,**kwargs): + skiphead=0, skiptail=0, every=1, **kwargs): + """read data from a file + - file might either be a string or a file instance (something, that + provides readlines()) + - each non-empty line, which does not match the commentpattern, is + considered to be a data row; columns are extracted by the splitline + method using tofloat=1 + - the last line before a data line matching the commentpattern and + containing further characters is considered as the title line; + the title list is extracted by the splitline method using tofloat=0 + - the first skiphead data lines are skiped + - the last skiptail data lines are skiped + - only every "every" data line is used (starting at the skiphead + 1 line) + - the number of columns is equalized between data and titles like + in the data constructor without setting maxcolumns + - further keyword arguments are passed to the constructor of data, + keyword arguments data, titles, and maxcolumns excluded""" if helper.isstring(file): file = open(file, "r") usetitles = [] @@ -199,14 +311,21 @@ class datafile(data): class sectionfile(_data): def __init__(self, file, sectionstr = "section", **kwargs): + """read data from a config-like file + - file might either be a string or a file instance (something, that + is valid in config.readfp()) + - each row is defined by a section in the config-like file (see + config module description) + - the columns for each row are defined by lines in the section file; + the title entries are used to identify the columns + - further keyword arguments are passed to the constructor of _data, + keyword arguments data and titles excluded""" config = ConfigParser.ConfigParser() config.optionxform = str - try: - file = file + '' - except TypeError: - config.readfp(file) - else: + if helper.isstring(file): config.readfp(open(file, "r")) + else: + config.readfp(file) usedata = [] usetitles = [sectionstr] sections = config.sections() diff --git a/pyx/graph.py b/pyx/graph.py index 0522746a..6c5eef62 100644 --- a/pyx/graph.py +++ b/pyx/graph.py @@ -174,21 +174,22 @@ class tick(frac): a tick is a frac enhanced by - self.ticklevel (0 = tick, 1 = subtick, etc.) - self.labellevel (0 = label, 1 = sublabel, etc.) - - self.text + - self.label (a string) and self.labelttrs (a list, defaults to []) When ticklevel or labellevel is None, no tick or label is present at that value. - When text is None, it should be automatically created (and stored), once the - an axis painter needs it.""" + When label is None, it should be automatically created (and stored), once the + an axis painter needs it. Classes, which implement _Itexter do precisely that.""" - def __init__(self, enum, denom, ticklevel=None, labellevel=None, text=None): + def __init__(self, enum, denom, ticklevel=None, labellevel=None, label=None, labelattrs=[]): frac.__init__(self, enum, denom) self.ticklevel = ticklevel self.labellevel = labellevel - self.text = text + self.label = label + self.labelattrs = labelattrs def merge(self, other): """merges two ticks together: - the lower ticklevel/labellevel wins - - when present, self.text is taken over; otherwise the others text is taken + - when present, self.label is taken over; otherwise the others label is taken - the ticks should be at the same position (otherwise it doesn't make sense) -> this is NOT checked """ @@ -196,11 +197,11 @@ class tick(frac): self.ticklevel = other.ticklevel if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel): self.labellevel = other.labellevel - if self.text is None: - self.text = other.text + if self.label is None: + self.label = other.label def __repr__(self): - return "tick(%r, %r, %s, %s, %s)" % (self.enum, self.denom, self.ticklevel, self.labellevel, self.text) + return "tick(%r, %r, %s, %s, %s)" % (self.enum, self.denom, self.ticklevel, self.labellevel, self.label) def _mergeticklists(list1, list2): @@ -229,37 +230,37 @@ def _mergeticklists(list1, list2): return list1 -def _mergetexts(ticks, texts): - """helper function to merge texts into ticks - - when texts is not None, the text of all ticks with +def _mergelabels(ticks, labels): + """helper function to merge labels into ticks + - when labels is not None, the label of all ticks with labellevel different from None are set - - texts need to be a sequence of sequences of strings, + - labels need to be a sequence of sequences of strings, where the first sequence contain the strings to be - used as texts for the ticks with labellevel 0, + used as labels for the ticks with labellevel 0, the second sequence for labellevel 1, etc. - when the maximum labellevel is 0, just a sequence of - strings might be provided as the texts argument + strings might be provided as the labels argument - IndexError is raised, when a sequence length doesn't match""" - if helper.issequenceofsequences(texts): - for text, level in zip(texts, xrange(sys.maxint)): - usetext = helper.ensuresequence(text) + if helper.issequenceofsequences(labels): + for label, level in zip(labels, xrange(sys.maxint)): + usetext = helper.ensuresequence(label) i = 0 for tick in ticks: if tick.labellevel == level: - tick.text = usetext[i] + tick.label = usetext[i] i += 1 if i != len(usetext): - raise IndexError("wrong sequence length of texts at level %i" % level) - elif texts is not None: - usetext = helper.ensuresequence(texts) + raise IndexError("wrong sequence length of labels at level %i" % level) + elif labels is not None: + usetext = helper.ensuresequence(labels) i = 0 for tick in ticks: if tick.labellevel == 0: - tick.text = usetext[i] + tick.label = usetext[i] i += 1 if i != len(usetext): - raise IndexError("wrong sequence length of texts") + raise IndexError("wrong sequence length of labels") class _Ipart: @@ -311,8 +312,8 @@ class manualpart: might be provided in ticks and labels - when labels is None and ticks is not None, the tick entries for ticklevel 0 are used for labels and vice versa (ticks<->labels) - - texts are applied to the resulting partition via the - mergetexts function (additional information available there) + - labels are applied to the resulting partition via the + mergelabels function (additional information available there) - mix specifies another partition to be merged into the created partition""" if ticks is None and labels is not None: @@ -357,7 +358,7 @@ class manualpart: ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, labellevel = 0) for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(self.labels)))]) - _mergetexts(ticks, self.texts) + _mergelabels(ticks, self.labels) return ticks @@ -413,7 +414,7 @@ class linpart: self.labels = (_ensurefrac(helper.ensuresequence(ticks)[0]),) else: self.labels = map(_ensurefrac, helper.ensuresequence(labels)) - self.texts = texts + self.labels = labels self.extendtick = extendtick self.extendlabel = extendlabel self.epsilon = epsilon @@ -454,7 +455,7 @@ class linpart: for i in range(len(self.labels)): ticks = _mergeticklists(ticks, self.getticks(min, max, self.labels[i], labellevel = i)) - _mergetexts(ticks, self.texts) + _mergelabels(ticks, self.labels) return ticks @@ -603,7 +604,7 @@ class logpart(linpart): self.labels = (helper.ensuresequence(ticks)[0],) else: self.labels = helper.ensuresequence(labels) - self.texts = texts + self.labels = labels self.extendtick = extendtick self.extendlabel = extendlabel self.epsilon = epsilon @@ -942,6 +943,181 @@ class axisrater: ################################################################################ +# texter +# texter automatically create labels for tick instances +################################################################################ + + +class _Itexter: + + def labels(self, ticks): + """fill the label attribute of ticks + - ticks is a list of instances of tick + - for each element of ticks the value of the attribute label is set to + a string appropriate to the attributes enum and denom of that tick + instance + - label attributes of the tick instances are just kept, whenever they + are not equal to None + - the method might add texsetting instances to the labelattrs attribute + of the ticks""" + + +class notexter: + """a texter, which does just nothing (I'm not sure, if this makes sense)""" + + __implements__ = _Itexter + + def labels(self, ticks): + pass + + +class rationaltexter: + """a texter creating rational labels (e.g. "a/b" or even "a \over b")""" + # XXX: we use divmod here to be more portable + + __implements__ = _Itexter + + def __init__(self, prefix="", suffix="", + enumprefix="", enumsuffix="", + denomprefix="", denomsuffix="", + equaldenom=0, minuspos=0, over=r"{{%s}\over{%s}}", + skipenum0=1, skipenum1=1, skipdenom1=1, + labelattrs=textmodule.mathmode): + """initializes the instance + - prefix and suffix (strings) are just added to the begin and to + the end of the label, respectively + - prefixenum and suffixenum (strings) are added to the labels + enumerator simularly + - prefixdenom and suffixdenom (strings) are added to the labels + denominator simularly + - the enumerator and denominator are +++TODO: KÜRZEN+++; however, + when equaldenom is set, the least common multiple of all + denominators is used + - minuspos is an integer, which determines the position, where the + minus sign has to be placed; the following values are allowed: + 0 - written immediately before the prefix + 1 - written immediately after the prefix + 2 - written immediately before the enumprefix + 3 - written immediately after the enumprefix + 4 - written immediately before the denomprefix + 5 - written immediately after the denomprefix + - over (string) is taken as a format string generating the + fraction bar; it has to contain exactly two string insert + operators "%s" - the first for the enumerator and the second + for the denominator; by far the most common examples are + r"{{%s}\over{%s}}" and "{{%s}/{%s}}" + - skipenum0 (boolean) just prints a zero instead of + the hole fraction, when the enumerator is zero; + all prefixes and suffixes are omitted as well + - skipenum1 (boolean) just prints the enumprefix directly followed + by the enumsuffix, when the enum value is one and at least either, + the enumprefix or the enumsuffix is present + - skipdenom1 (boolean) just prints the enumerator instead of + the hole fraction, when the denominator is one + - labelattrs is a sequence of texsetting instances; also just a single + instance is allowed; an empty sequence is allowed as well, but None + is not valid""" + self.prefix = prefix + self.suffix = suffix + self.enumprefix = enumprefix + self.enumsuffix = enumsuffix + self.denomprefix = denomprefix + self.denomsuffix = denomsuffix + self.equaldenom = equaldenom + self.minuspos = minuspos + if self.minuspos < 0 or self.minuspos > 5: + raise RuntimeError("invalid minuspos") + self.over = over + self.skipenum0 = skipenum0 + self.skipenum1 = skipenum1 + self.skipdenom1 = skipdenom1 + self.labelattrs = helper.ensuresequence(labelattrs) + + def gcd(self, m, n): + """returns the greates common divisor + - m and n must be non-negative integers""" + if m < n: + m, n = n, m + while n > 0: + m, (dummy, n) = n, divmod(m, n) # ensure portable integer division + return m + + def lcm(self, *n): + """returns the least common multiple of all elements in n + - the elements of n must be integers + - return None if the number of elements is zero""" + "TODO: fill it from knuth" + pass + + def labels(self, ticks): + # the temporary variables fracenum, fracdenom, and fracminus are + # inserted into all tick instances, where label is not None + for tick in ticks: + if tick.label is None: + tick.fracminus = 1 + tick.fracenum = tick.enum + tick.fracdenom = tick.denom + if tick.fracenum < 0: + tick.fracminus *= -1 + tick.fracenum *= -1 + if tick.fracdenom < 0: + tick.fracminus *= -1 + tick.fracdenom *= -1 + gcd = self.gcd(tick.fracenum, tick.fracdenom) + (tick.fracenum, dummy1), (tick.fracdenom, dummy2) = divmod(tick.enum, gcd), divmod(tick.fracdenom, gcd) + if self.equaldenom: + equaldenom = self.lcm([tick.fracdenom for tick in ticks if tick.label is None]) + if equaldenom is not None: + for tick in ticks: + if tick.label is None: + factor, dummy = divmod(equaldenom, tick.fracdenom) + assert dummy != 0, "internal error: wrong lowest common multiple?" # TODO: remove that check + tick.fracenum, tick.fracdenom = factor * tick.fracenum, factor * tick.fracdenom + for tick in ticks: + if tick.label is None: + if tick.fracminus == -1: + tick.fracminus = "-" + else: + tick.fracminus = "" + if self.skipenum0 and tick.fracenum == 0: + if self.minuspos == 2 or self.minuspos == 3: + tick.fracenum = "%s0" % tick.fracminus + else: + tick.fracenum = tick.fracenum + elif self.skipenum1 and tick.fracenum == 1 and (len(self.enumprefix) or len(self.enumsuffix)): + if self.minuspos == 2: + tick.fracenum = "%s%s%s" % (tick.fracminus, self.enumprefix, self.enumsuffix) + elif self.minuspos == 3: + tick.fracenum = "%s%s%s" % (self.enumprefix, tick.fracminus, self.enumsuffix) + else: + tick.fracenum = "%s%s" % (self.enumprefix, self.enumsuffix) + else: + if self.minuspos == 2: + tick.fracenum = "%s%s%i%s" % (tick.fracminus, self.enumprefix, tick.fracenum, self.enumsuffix) + elif self.minuspos == 3: + tick.fracenum = "%s%s%i%s" % (self.enumprefix, tick.fracminus, tick.fracenum, self.enumsuffix) + else: + tick.fracenum = "%s%i%s" % (self.enumprefix, tick.fracenum, self.enumsuffix) + if self.skipdenom1 and tick.fracdenom == 1 and self.minuspos != 4 and self.minuspos != 5: + frac = tick.fracenum + else: + if self.minuspos == 4: + tick.fracdenom = "%s%s%i%s" % (tick.fracminus, self.denomprefix, tick.fracdenom, self.denomsuffix) + elif self.minuspos == 5: + tick.fracdenom = "%s%s%i%s" % (self.denomprefix, tick.fracminus, tick.fracdenom, self.denomsuffix) + else: + tick.fracdenom = "%s%i%s" % (self.denomprefix, tick.fracdenom, self.denomsuffix) + frac = self.over % (tick.fracenum, tick.fracdenom) + if self.minuspos == 0: + tick.label = "%s%s%s%s" % (tick.fracminus, self.prefix, frac, self.suffix) + elif self.minuspos == 1: + tick.label = "%s%s%s%s" % (self.prefix, tick.fracminus, frac, self.suffix) + else: + tick.label = "%s%s%s" % (self.prefix, frac, self.suffix) + + + +################################################################################ # axis painter ################################################################################ @@ -1047,14 +1223,6 @@ class axispainter(axistitlepainter): self.suffix1 = suffix1 axistitlepainter.__init__(self, **args) - def gcd(self, m, n): - # greates common divisor, m & n must be non-negative - if m < n: - m, n = n, m - while n > 0: - m, (dummy, n) = n, divmod(m, n) - return m - def attachsuffix(self, tick, str): if self.suffix0 or tick.enum: if tick.suffix is not None and not self.suffix1: @@ -1157,17 +1325,17 @@ class axispainter(axistitlepainter): tick.decfraclength = None if self.fractype == self.fractypeauto: if tick.suffix is not None: - tick.text = self.ratfrac(tick) + tick.label = self.ratfrac(tick) else: - tick.text = self.expfrac(tick, self.expfracminexp) - if tick.text is None: - tick.text = self.decfrac(tick) + tick.label = self.expfrac(tick, self.expfracminexp) + if tick.label is None: + tick.label = self.decfrac(tick) elif self.fractype == self.fractypedec: - tick.text = self.decfrac(tick) + tick.label = self.decfrac(tick) elif self.fractype == self.fractypeexp: - tick.text = self.expfrac(tick) + tick.label = self.expfrac(tick) elif self.fractype == self.fractyperat: - tick.text = self.ratfrac(tick) + tick.label = self.ratfrac(tick) else: raise ValueError("fractype invalid") if textmodule.mathmode not in helper.getattrs(tick.labelattrs, textmodule._texsetting, []): @@ -1185,12 +1353,12 @@ class axispainter(axistitlepainter): tick.labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel) if tick.labelattrs is not None: tick.labelattrs = list(helper.ensuresequence(tick.labelattrs)) - if tick.text is None: + if tick.label is None: tick.suffix = axis.suffix self.createtext(tick) if self.labeldirection is not None: tick.labelattrs += [trafo.rotate(self.reldirection(self.labeldirection, tick.dx, tick.dy))] - tick.textbox = textmodule._text(tick.x, tick.y, tick.text, *tick.labelattrs) + tick.textbox = textmodule._text(tick.x, tick.y, tick.label, *tick.labelattrs) if self.decfracequal: maxdecfraclength = max([tick.decfraclength for tick in axis.ticks if tick.labellevel is not None and tick.labelattrs is not None and @@ -1199,10 +1367,10 @@ class axispainter(axistitlepainter): if (tick.labellevel is not None and tick.labelattrs is not None and tick.decfraclength is not None): - tick.text = self.decfrac(tick, maxdecfraclength) + tick.label = self.decfrac(tick, maxdecfraclength) for tick in axis.ticks: if tick.labellevel is not None and tick.labelattrs is not None: - tick.textbox = textmodule._text(tick.x, tick.y, tick.text, *tick.labelattrs) + tick.textbox = textmodule._text(tick.x, tick.y, tick.label, *tick.labelattrs) if len(axis.ticks) > 1: equaldirection = 1 for tick in axis.ticks[1:]: @@ -1739,7 +1907,7 @@ class linkaxis: if ticklevel is not None or labellevel is not None: result.append(tick(_tick.enum, _tick.denom, ticklevel, labellevel)) return result - # XXX: don't forget to calculate new text positions as soon as this is moved + # XXX: don't forget to calculate new label positions as soon as this is moved # outside of the paint method (when rating is moved into the axispainter) def getdatarange(self): diff --git a/pyx/text.py b/pyx/text.py index 2b45e242..2c4dd005 100644 --- a/pyx/text.py +++ b/pyx/text.py @@ -818,11 +818,11 @@ class TexResultError(Exception): def __str__(self): "prints a detailed report about the problem" return ("%s\n" % self.description + - "The expression passed to TeX was:\n" + + "The expression passed to TeX was:\n" " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() + - "The return message from TeX was:\n" + + "The return message from TeX was:\n" " %s\n" % self.texrunner.texmessage.replace("\n", "\n ").rstrip() + - "After parsing this message, the following was left:\n" + + "After parsing this message, the following was left:\n" " %s" % self.texrunner.texmessageparsed.replace("\n", "\n ").rstrip()) @@ -864,11 +864,11 @@ class _texmessagestart(texmessage): texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():] try: texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1] - except IndexError: + except (IndexError, ValueError): raise TexResultError("TeX running startup file failed", texrunner) try: texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1] - except IndexError: + except (IndexError, ValueError): raise TexResultError("TeX scrollmode check failed", texrunner) @@ -898,7 +898,7 @@ class _texmessageinputmarker(texmessage): def check(self, texrunner): try: - s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker(%s)" % texrunner.executeid, 1) + s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1) texrunner.texmessageparsed = s1 + s2 except (IndexError, ValueError): raise TexResultError("PyXInputMarker expected", texrunner) @@ -909,7 +909,7 @@ class _texmessagepyxbox(texmessage): __implements__ = _Itexmessage - pattern = re.compile(r"PyXBox\(page=(?P\d+),lt=-?\d*((\d\.?)|(\.?\d))\d*pt,rt=-?\d*((\d\.?)|(\.?\d))\d*pt,ht=-?\d*((\d\.?)|(\.?\d))\d*pt,dp=-?\d*((\d\.?)|(\.?\d))\d*pt\)") + pattern = re.compile(r"PyXBox:page=(?P\d+),lt=-?\d*((\d\.?)|(\.?\d))\d*pt,rt=-?\d*((\d\.?)|(\.?\d))\d*pt,ht=-?\d*((\d\.?)|(\.?\d))\d*pt,dp=-?\d*((\d\.?)|(\.?\d))\d*pt:") def check(self, texrunner): m = self.pattern.search(texrunner.texmessageparsed) @@ -928,7 +928,7 @@ class _texmessagepyxpageout(texmessage): try: s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1) texrunner.texmessageparsed = s1 + s2 - except IndexError: + except (IndexError, ValueError): raise TexResultError("PyXPageOutMarker expected", texrunner) @@ -952,7 +952,7 @@ class _texmessagetexend(texmessage): try: s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1) texrunner.texmessageparsed = s1 + s2 - except IndexError: + except (IndexError, ValueError): pass dvipattern = re.compile(r"Output written on %s\.dvi \((?P\d+) pages?, \d+ bytes\)\." % texrunner.texfilename) m = dvipattern.search(texrunner.texmessageparsed) @@ -966,12 +966,12 @@ class _texmessagetexend(texmessage): try: s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1) texrunner.texmessageparsed = s1 + s2 - except IndexError: + except (IndexError, ValueError): raise TexResultError("no dvifile expected") try: s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1) texrunner.texmessageparsed = s1 + s2 - except IndexError: + except (IndexError, ValueError): raise TexResultError("TeX logfile message expected") @@ -1001,6 +1001,13 @@ class _texmessageload(texmessage): pattern = re.compile(r"\((?P[^()\s\n]+)[^()]*\)") def baselevels(self, s, maxlevel=1, brackets="()"): + """strip parts of a string above a given bracket level + - return a modified (some parts might be removed) version of the string s + where all parts inside brackets with level higher than maxlevel are + removed + - if brackets do not match (number of left and right brackets is wrong + or at some points there were more right brackets than left brackets) + just return the unmodified string""" level = 0 highestlevel = 0 res = "" @@ -1013,7 +1020,7 @@ class _texmessageload(texmessage): res += c if c == brackets[1]: level -= 1 - if not level and highestlevel > 0: + if level == 0 and highestlevel > 0: return res def check(self, texrunner): @@ -1021,6 +1028,7 @@ class _texmessageload(texmessage): if lowestbracketlevel is not None: m = self.pattern.search(lowestbracketlevel) while m: + print m.group("filename") if os.access(m.group("filename"), os.R_OK): lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():] else: @@ -1032,13 +1040,24 @@ class _texmessageload(texmessage): class _texmessagegraphicsload(_texmessageload): """validates the inclusion of files as the graphics packages writes it - - works like _texmessageload, but using "<" and ">" as delimiters""" + - works like _texmessageload, but using "<" and ">" as delimiters + - filename must end with .eps and no further text is allowed""" - pattern = re.compile(r"<(?P[^()\s\n]+)[^()]*>") + pattern = re.compile(r"<(?P[^>]+.eps)>") def baselevels(self, s, brackets="<>", **args): - _texmessageload.baselevels(self, s, brackets=brackets, **args) + return _texmessageload.baselevels(self, s, brackets=brackets, **args) + +#class _texmessagepdfmapload(_texmessageload): +# """validates the inclusion of files as the graphics packages writes it +# - works like _texmessageload, but using "{" and "}" as delimiters +# - filename must end with .map and no further text is allowed""" +# +# pattern = re.compile(r"{(?P[^}]+.map)}") +# +# def baselevels(self, s, brackets="{}", **args): +# return _texmessageload.baselevels(self, s, brackets=brackets, **args) class _texmessageignore(_texmessageload): @@ -1482,6 +1501,9 @@ class texrunner: texmessageend=texmessage.texend, texmessagedefaultpreamble=texmessage.load, texmessagedefaultrun=None): + mode = mode.lower() + if mode != "tex" and mode != "latex" and mode != "pdftex" and mode != "pdflatex": + raise ValueError("mode \"TeX\", \"LaTeX\", \"pdfTeX\", or \"pdfLaTeX\" expected") self.mode = mode self.lfs = lfs self.docclass = docclass @@ -1554,31 +1576,39 @@ class texrunner: self.texruns = 1 oldpreamblemode = self.preamblemode self.preamblemode = 1 - self.execute("\\scrollmode\n\\raiseerror%\n" + # switch to and check scrollmode - "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" + # just the PyX Logo - "\\gdef\\PyXHAlign{0}%\n" + # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0 - "\\newbox\\PyXBox%\n" + # PyXBox will contain the output - "\\newbox\\PyXBoxHAligned%\n" + # PyXBox will contain the horizontal aligned output - "\\newdimen\\PyXDimenHAlignLT%\n" + # PyXDimenHAlignLT/RT will contain the left/right extent + self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode + "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo + "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0 + "\\newbox\\PyXBox%\n" # PyXBox will contain the output + "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output + "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent "\\newdimen\\PyXDimenHAlignRT%\n" + _texsettingpreamble + # insert preambles for texsetting macros - "\\def\\ProcessPyXBox#1#2{%\n" + # the ProcessPyXBox definition (#1 is expr, #2 is page number) - "\\setbox\\PyXBox=\\hbox{{#1}}%\n" + # push expression into PyXBox - "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" + # calculate the left/right extent - "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n" + - "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n" + - "\\gdef\\PyXHAlign{0}%\n" + # reset the PyXHAlign to the default 0 - "\\immediate\\write16{PyXBox(page=#2," + # write page and extents of this box to stdout - "lt=\\the\\PyXDimenHAlignLT," + - "rt=\\the\\PyXDimenHAlignRT," + - "ht=\\the\\ht\\PyXBox," + - "dp=\\the\\dp\\PyXBox)}%\n" + - "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" + # align horizontally - "\\ht\\PyXBoxHAligned0pt%\n" + # baseline alignment (hight to zero) - "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" + # shipout PyXBox to Page 80.121.88. - "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker(#1)}}", # write PyXInputMarker() to stdout + "\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number) + "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox + "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent + "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n" + "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n" + "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0 + "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout + "lt=\\the\\PyXDimenHAlignLT," + "rt=\\the\\PyXDimenHAlignRT," + "ht=\\the\\ht\\PyXBox," + "dp=\\the\\dp\\PyXBox:}%\n" + "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally + "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero) + "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88. + "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}", # write PyXInputMarker to stdout *self.texmessagestart) os.remove("%s.tex" % self.texfilename) + if self.mode == "pdftex" or self.mode == "pdflatex": + self.execute("\\pdfoutput=1%\n" + "\\def\\marker#1{%\n" + #"\\pdfsavepos%\n" + "\\write16{PyXMarker:name=#1," + "xpos=\\the\\pdflastxpos," + "ypos=\\the\\pdflastypos:}%\n" + "}%\n") if self.mode == "tex": try: LocalLfsName = str(self.lfs) + ".lfs" @@ -1610,7 +1640,7 @@ class texrunner: self.execute(lfsdef) self.execute("\\normalsize%\n") self.execute("\\newdimen\\linewidth%\n") - elif self.mode == "latex": + elif self.mode == "latex" or self.mode == "pdflatex": if self.docopt is not None: self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass), *self.texmessagedocclass) else: @@ -1618,7 +1648,7 @@ class texrunner: self.preamblemode = oldpreamblemode self.executeid += 1 if expr is not None: # TeX/LaTeX should process expr - self.expectqueue.put_nowait("PyXInputMarker(%i)" % self.executeid) + self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid) if self.preamblemode: self.expr = ("%s%%\n" % expr + "\\PyXInput{%i}%%\n" % self.executeid) @@ -1628,7 +1658,7 @@ class texrunner: "\\PyXInput{%i}%%\n" % self.executeid) else: # TeX/LaTeX should be finished self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename) - if self.mode == "latex": + if self.mode == "latex" or self.mode == "pdflatex": self.expr = "\\end{document}\n" else: self.expr = "\\end\n" @@ -1701,8 +1731,8 @@ class texrunner: raise TexRunsError if mode is not None: mode = mode.lower() - if mode != "tex" and mode != "latex": - raise ValueError("mode \"TeX\" or \"LaTeX\" expected") + if mode != "tex" and mode != "latex" and mode != "pdftex" and mode != "pdflatex": + raise ValueError("mode \"TeX\", \"LaTeX\", \"pdfTeX\", or \"pdfLaTeX\" expected") self.mode = mode if lfs is not None: self.lfs = lfs @@ -1793,7 +1823,7 @@ class texrunner: helper.checkattr(args, allowmulti=(texmessage,)) self.execute(expr, *helper.getattrs(args, texmessage, default=self.texmessagedefaultpreamble)) - PyXBoxPattern = re.compile(r"PyXBox\(page=(?P\d+),lt=(?P-?\d*((\d\.?)|(\.?\d))\d*)pt,rt=(?P-?\d*((\d\.?)|(\.?\d))\d*)pt,ht=(?P-?\d*((\d\.?)|(\.?\d))\d*)pt,dp=(?P-?\d*((\d\.?)|(\.?\d))\d*)pt\)") + PyXBoxPattern = re.compile(r"PyXBox:page=(?P\d+),lt=(?P-?\d*((\d\.?)|(\.?\d))\d*)pt,rt=(?P-?\d*((\d\.?)|(\.?\d))\d*)pt,ht=(?P-?\d*((\d\.?)|(\.?\d))\d*)pt,dp=(?P-?\d*((\d\.?)|(\.?\d))\d*)pt:") def _text(self, x, y, expr, *args): """create text by passing expr to TeX/LaTeX @@ -1811,7 +1841,7 @@ class texrunner: if self.texdone: raise TexDoneError if self.preamblemode: - if self.mode == "latex": + if self.mode == "latex" or self.mode == "pdflatex": self.execute("\\begin{document}", *self.texmessagebegindoc) self.preamblemode = 0 helper.checkattr(args, allowmulti=(_texsetting, texmessage, trafo._trafo, base.PathStyle)) -- 2.11.4.GIT