Skip to content

Controls

yex.control.Control(is_long=False, is_outer=False, from_human=True, name=None, doc=None, *args, **kwargs) #

Controls are callable procedures. They live within a ControlsTable, within a document.

Each yex.control.Control is usually referred to by at least one yex.parse.ControlName token object in a given document.

Some abstract subclasses of Control#

The subclasses which are most important to understand are nearest to the top of this list.

  • Expandable: a control which expands into tokens. For example, all macros are expandables. They have no side-effects; they simply expand.
  • Unexpandable: a built-in control which does something other than merely expanding. For example, Hrule inserts a horizontal rule.
  • Parameter: an Unexpandable which has a value. For example, the value of Year is the number of the current year in the Common Era.
  • Array: a control containing multiple entries, which we call registers, such as \count0 and \count1.
  • Fontsetter: a control created by the user using the \font control. When you call it, it changes the current font.
  • Documentfield: a parameter control which refers to a field in the Document. You don't really need to know about these.

You can implement a new control subclass by subclassing Expandable or Unexpandable. But it's generally easier to use the @control decorator on a function.

yex.control vs yex.keyword#

The package yex.control contains classes which help to make controls, as in the list above. The subclasses which actually represent TeΧ keywords live in yex.keyword.

About class identifiers#

TeΧ controls are named in all lowercase, with a leading backslash, thus: \kern. But we can't represent the backslash in a Python identifier, and Python classes traditionally have names in titlecase. So the class for \kern is Kern.

Because there are some funky kinds of control out there, there are a few more ways of naming controls:

  • a class whose name begins X_ represents a TeΧ control with the same name, lowercased, with the X_ stripped.
  • a class whose name is A_ followed by four hex digits giving a Unicode codepoint represents the TeΧ control whose name consists only of that character. This is useful for active characters.
  • a class whose name is S_ followed by four hex digits giving a Unicode codepoint represents the TeΧ control whose name consists only of a backslash followed by that character.
Source code in yex/control/control.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def __init__(self,
             is_long: bool = False,
             is_outer: bool = False,
             from_human: bool = True,
             name: Union[str, None] = None,
             doc: Union['yex.Document', None] = None,
             *args, **kwargs):

    self.is_long = is_long
    self.is_outer = is_outer
    self.from_human = from_human
    self.doc = doc

    if name is None:
        self.name = self.__class__.__name__.lower()
    else:
        self.name = name

conditional = False class-attribute instance-attribute #

Whether this control affects conditional execution: \if, \else, and so on.

doc = doc class-attribute instance-attribute #

The document we belong to.

even_if_not_expanding = False class-attribute instance-attribute #

Whether this control should be executed even when the parser isn't executing. There are only a very few of these.

TeΧbook: 215

from_human = from_human class-attribute instance-attribute #

False if yex itself inserted this control; True if it was human-generated. The only current case where this is False is the automatic \indent at the start of a paragraph.

identifier property #

A good string to use for looking up this control in a document.

In practice, it could be stored under a different string as well, or instead, or it might not be stored at all. But this is often a reasonable shot.

is_array = False class-attribute instance-attribute #

Whether this control is an array, where you can look up entries by an index number. See yex.control.Array.

is_long = is_long class-attribute instance-attribute #

Whether this control is a macro whose arguments can include \par.

is_outer = is_outer class-attribute instance-attribute #

Whether this control is a macro which can't be used inside other macros. (This is an oversimplification; see the TeΧbook for the full details.)

TeΧbook: p205

is_queryable = False class-attribute instance-attribute #

Whether this control behaves differently when it's the target of an assignment (often known as an "lvalue"). For this behaviour, you can call the query() method.

name = None class-attribute instance-attribute #

The name of the control. If you supply None to the constructor, this will be initialised with the name of the control class, lowercased. For example, Year will have name=="year".

value property writable #

Some controls have values. For example, the value of [Year](yex.control.Year.md) is the current year in the Common Era.

The type of the value can be anything at all. If a control has no other interesting value to give, then its value should be itself.

Some values can be set; if you attempt to set a value which can't be set, you will get AttributeError: this is the same behaviour as with Python properties.

The getter/setter behaviour is implemented under the bonnet by the methods _get_value() and _set_value(). This is because Python gets rather baroque about inheritance and properties.

__call__(*args, **kwargs) #

Run the procedure.

Source code in yex/control/control.py
195
196
197
198
199
def __call__(self, *args, **kwargs) -> Any:
    """
    Run the procedure.
    """
    raise NotImplementedError()

from_serial(state) classmethod #

Deserialise a control.

Source code in yex/control/control.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
@classmethod
def from_serial(self, state:dict) -> Self:
    """
    Deserialise a control.
    """
    name = state['control'][0].upper() + \
            state['control'][1:].lower()

    if not hasattr(yex.control, name):
        raise KeyError(state['control'])

    result = getattr(yex.control, name)()

    if 'value' in state:
        result.value = state['value']

    return result

get_arguments_from_parser(types, parser) classmethod #

Finds arguments for a function, given a list of its parameters. This is a helper function for the @control decorator.

Each entry in the list of parameters is either a bare string, giving the name of the parameter, or a (string, type) pair, giving the name and type annotation of the parameter. The result will be a list of values found for each parameter, in the same order.

How we find the values#

In this section, all mentions of yex's own types include their subclasses. For example, if we mention a Control, it includes all the subclasses of Control.

If an entry has a type annotation#

...

If an entry doesn't have a type annotation#

...

If an entry's name ends with "all_args"#

...

Raises:

Type Description
NeededSomethingElseError

if the token stream can't be construed to fit the parameters

WeirdControlNameError

if there is no type annotation, and the name doesn't suggest what to look for

WeirdControlAnnotationError

if the annotation is not a type we know how to produce

Source code in yex/control/control.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
@classmethod
def get_arguments_from_parser(cls,
                              types: List[Union[
                                  Tuple[str, Type],
                                  str,
                                  ]],
                              parser: 'yex.parse.Parser',
                              ) -> List[Any]:
    """
    Finds arguments for a function, given a list of its
    parameters. This is a helper function for
    [the `@control` decorator](yex.decorator.control.md).

    Each entry in the list of parameters is either a
    bare string, giving the name of the parameter,
    or a (string, type) pair, giving the name and
    type annotation of the parameter. The result will
    be a list of values found for each parameter,
    in the same order.

    # How we find the values

    In this section, all mentions of yex's own types
    include their subclasses. For example, if we
    mention a Control, it includes all the subclasses of Control.

    ## If an entry has a type annotation

    ...

    ## If an entry doesn't have a type annotation

    ...

    # If an entry's name ends with `"all_args"`

    ...

    Raises:
        NeededSomethingElseError: if the token stream
            can't be construed to fit the parameters
        WeirdControlNameError: if there is no type
            annotation, and the name doesn't suggest
            what to look for
        WeirdControlAnnotationError: if the annotation
            is not a type we know how to produce
    """

    result = []

    ALL_ARGS_SUFFIX = 'all_args'

    if parser is None:
        raise yex.exception.ParserWasNoneError()

    t = parser.another(
            level = 'reading',
            on_eof = 'raise',
            )

    logger.debug('args: Looking for these arguments: %s', types)
    logger.debug('args: from this Parser: %s', parser)

    for arg in types:

        if isinstance(arg, tuple):
            the_name, the_type = arg
            logger.debug('args: finding arg "%s", annotated as %s',
                    the_name, the_type)

            if isinstance(the_type, str):
                the_type = globals()[the_type]
        else:
            the_name = arg
            the_type = None
            logger.debug('args: finding arg "%s", with no annotation',
                    the_name)

        if the_name.endswith(ALL_ARGS_SUFFIX) and the_type in [None, 'str']:
            value = ''

            level = the_name[:-len(ALL_ARGS_SUFFIX)-1]
            logger.debug('args: slurping up tokens at level "%s"',
                    level)

            for t in parser.another(
                    level=level,
                    bounded='single',
                    on_eof='exhaust',
                    ):
                value += str(t)

            logger.debug('args: which gives us: %s',
                    value)

        elif the_name=='parser' and (
                the_type is None or issubclass(the_type, yex.parse.Parser)
                ):
            value = parser

        elif the_name=='doc' and the_type in {None,
                                              yex.document.Document}:
            value = parser.doc

        elif the_name=='optional_equals' and the_type in {None, str}:
            value = parser.eat_optional_char('=')

        elif the_type is None:
            logger.debug(
                       "args: can't work that out with no annotation")

            raise yex.exception.WeirdControlNameError(
                    argname = the_name,
                    )

        elif issubclass(the_type, int):
            logger.debug('args: looking for an integer')

            value = int(yex.value.Number.from_parser(t))

        elif issubclass(the_type, yex.parse.Location):
            value = t.location

        elif issubclass(the_type, (
                yex.parse.Token,
                yex.control.Control,
                )):

            logger.debug('args: looking for a %s in its own right',
                    the_type.__name__)

            # These might be in the token stream,
            # in their own right.

            value = t.next()

            if not isinstance(value, the_type):
                logger.debug('args:   -- but we found a %s',
                        value.__class__.__name__)

                raise yex.exception.NeededSomethingElseError(
                        needed = the_type,
                        problem = value,
                        )

        elif issubclass(the_type, (
                yex.value.Value,
                yex.box.Gismo,
                yex.filename.Filename,
                )):

            # These can be constructed from the token stream.

            logger.debug('args: constructing a %s',
                    the_type.__name__)

            value = the_type.from_parser(t)

        else:
            logger.debug(
                    "args: can't work out an annotation of %s",
                    the_type.__name__)

            raise yex.exception.WeirdControlAnnotationError(
                    arg = the_name,
                    control = None,
                    annotation = the_type,
                    )

        logger.debug("args:  -- so %s == %s", the_name, value)
        result.append(value)

    logger.debug("args: result: %s", result)
    return result