Module Loading and Extension Initialization
There are three different ways in which byexample can be extended:
- define zones where to find examples
- support new languages: how to find them and how to run them
- perform arbitrary actions during the execution
You can see a more-in-depth documentation in each section above but all the
extensions are classes that inherit one for the main extension classes:
ZoneDelimiter, ExampleFinder, ExampleParser, ExampleRunner and Concern
>>> from byexample.finder import ZoneDelimiter, ExampleFinder
>>> from byexample.parser import ExampleParser
>>> from byexample.runner import ExampleRunner
>>> from byexample.concern import Concern
One or more of these extension classes (your classes) must be written in one or more Python module, as any other Python code.
byexample will load all the modules located in the folder defined in
the command line.
Error on module load
byexample will catch any error during the loading of a module,
typically a SyntaxError or an ImporError, and it will print a nice
message:
$ byexample -m test/ds/bad/syntax/ -l python --dry docs/languages/python.md # byexample: +norm-ws
[!] From '<...>test/ds/bad/syntax' loading module 'm' failed. Skipping.
invalid syntax (m.py, line 2)
<...>
Rerun with -vvv to get a full stack trace.
Running with -vvv, you will get the full stack too:
$ byexample -m test/ds/bad/syntax/ -l python --dry -vvv docs/languages/python.md # byexample: +norm-ws
[!] From '<...>test/ds/bad/syntax' loading module 'm' failed. Skipping.
Traceback (most recent call last):
<...>
File "<...>test/ds/bad/syntax/m.py", line 2
<...>
SyntaxError: <...>
<...>
When a module fails to load, byexample will skip it and continue with
the loading of the rest of the modules (other files).
Extension initialization
Once a module is loaded, byexample will search for any class that
inherits from one of the extension classes (or a subclass of them).
For each class found, it is initialized calling its __init__ method
passing several keyword-only arguments.
Among them your class will receive ns, sharer and cfg.
The namespace ns and the sharer are used for managing concurrency in the
case of your extension requires coordination between the workers
and it is documented in the
concurrency model.
The cfg is a Config object that holds all the configuration of
byexample.
All the keyword-only arguments that your __init__ will receive will be
also an attribute of cfg (with the exception of ns and sharer).
The following two __init__ are equivalent:
>>> class MyParserOldStyle(ExampleParser):
... def __init__(self, verbosity, encoding, **kargs):
... ExampleParser.__init__(self, verbosity=verbosity, encoding=encoding, **kargs)
...
... # Use these two config directly, "captured" by __init__
... # as keyword-only arguments
... print(verbosity)
... print(encoding)
>>> class MyParserNewStyle(ExampleParser):
... def __init__(self, cfg, **kargs):
... ExampleParser.__init__(self, cfg=cfg, **kargs)
...
... # Use these two config from the "captured" cfg
... print(cfg.verbosity)
... print(cfg.encoding)
Moreover, once the extension parent class is initialized, the extension
acquires a cfg property that can be used to read the configuration so
the following __init__ is also equivalent (and simpler).
>>> class MyParserNewStyle(ExampleParser):
... def __init__(self, **kargs):
... ExampleParser.__init__(self, **kargs)
...
... # Use these two config from the self.cfg property
... print(self.cfg.verbosity)
... print(self.cfg.encoding)
...
... def other_method(self):
... # self.cfg property is available during the whole lifetime
... # of the extension
... print(self.cfg.verbosity)
New in
byexample 11.0.0: before11.0.0, the only way to read the configuration was “capturing” them in the__init__and optionally stored manually likeself.encoding = encoding(seeMyParserOldStyle) From11.0.0, thecfgis available and you can use it directly (seeMyParserNewStyle)
Note: Python’s
superis not supported. Your subclasses should call the parent class’ method explicitly likeExampleParser.__init__(self, **kargs)instead ofsuper().__init__(**kargs)
Errors on initialization
byexample will capture any exception during the initialization and it
will display an error.
Here, this extension access non-set attribute and byexample will tell
you:
>>> class BadInit(Concern):
... def __init__(self, **kargs):
... Concern.__init__(self, **kargs)
...
... # XXX This will fail and we expect the exception to be caught
... # by byexample initialization process
... print(self.noattr)
$ byexample -m test/ds/bad/init/ -l python --dry docs/languages/python.md # byexample: +norm-ws
[!] Something went wrong initializing byexample:
From '<...>test/ds/bad/init' module 'init_failed'
Instantiation of BadInit failed: 'BadInit' object has no attribute 'noattr'
<...>
Missing to initialize parent class
You are required to call parent class’ __init__ passing
all the keyword arguments received by your subclass.
byexample will check that and it will complain if you didn’t
initialized the parent class.
>>> class BadConcernOldStyle(Concern):
... def __init__(self, verbosity, encoding, **kargs):
... # XXX Not calling Concern.__init__ is an error
... # This code will not fail but byexample will detect
... # and emit the error
... print(verbosity)
... print(encoding)
$ byexample -m test/ds/bad/init_not_called/chk/ -l python --dry docs/languages/python.md # byexample: +norm-ws
[!] Something went wrong initializing byexample:
From '<...>test/ds/bad/init_not_called/chk' module 'badconcernoldstyle'
The object of class BadConcernOldStyle did not call the constructor of Concern.
<...>
In the case of the new style extension (since byexample 11.0.0), accessing to
the cfg property will fail even before byexample has a chance to do
the check.
>>> class BadConcernNewStyle(Concern):
... def __init__(self, **kargs):
... # XXX Not calling Concern.__init__ is an error
... # Because cfg is not properly initialized, you will get
... # an error here
... print(self.cfg.verbosity)
... print(self.cfg.encoding)
$ byexample -m test/ds/bad/init_not_called/cfg/ -l python --dry docs/languages/python.md # byexample: +norm-ws
[!] Something went wrong initializing byexample:
From '<...>test/ds/bad/init_not_called/cfg' module 'badconcernnewstyle'
Instantiation of BadConcernNewStyle failed: The cfg property is not set.
Did you forget to call __init__ on an extension parent class?
<...>
PexpectMixin initialization on ExampleRunner
If you are implemented an ExampleRunner subclass (perhaps, while you are supporting a new language),
chances are that you are using the PexpectMixin.
This mixin heavily simplify the code needed to interact with an
interpreter/runner and it is designed to work together with
ExampleRunner.
Therefore, the PexpectMixin cannot be used by classes that don’t
inherit from ExampleRunner (directly or indirectly):
>>> from byexample.runner import PexpectMixin
>>> from byexample.concern import Concern
>>> class BadNonRunner(Concern, PexpectMixin):
... def __init__(self, **kargs):
... Concern.__init__(self, **kargs)
...
... # XXX We cannot inherit from PexpectMixin if we don't
... # inherit from ExampleRunner too
... PexpectMixin.__init__(
... self, PS1_re=r'\(gdb\)[ ]', any_PS_re=r'\(gdb\)[ ]'
... )
$ byexample -m test/ds/bad/pexpect_not_runner/ -l python --dry docs/languages/python.md # byexample: +norm-ws
[!] Something went wrong initializing byexample:
From '<...>test/ds/bad/pexpect_not_runner' module 'non_runner'
Instantiation of BadNonRunner failed: The class
BadNonRunner that inherits from PexpectMixin must also inherit from ExampleRunner.
<...>
PexpectMixin must be initialized after ExampleRunner otherwise you
will receive an error:
>>> from byexample.runner import ExampleRunner, PexpectMixin
>>> class BadRunner(ExampleRunner, PexpectMixin):
... def __init__(self, **kargs):
... # XXX Calling PexpectMixin before ExampleRunner.__init__ is an error
... PexpectMixin.__init__(
... self, PS1_re=r'\(gdb\)[ ]', any_PS_re=r'\(gdb\)[ ]'
... )
...
... ExampleRunner.__init__(self, **kargs)
$ byexample -m test/ds/bad/pexpect_init/ -l python --dry docs/languages/python.md # byexample: +norm-ws
[!] Something went wrong initializing byexample:
From '<...>test/ds/bad/pexpect_init' module 'badrunner'
Instantiation of BadRunner failed: You need to call
ExampleRunner.__init__ (or its subclass) before calling PexpectMixin.__init__ in BadRunner.
<...>
Extension’s target (or language)
Each ZoneDelimiter, ExampleFinder and Concern defines
a target and in the case of ExampleParser and ExampleRunner a language.
The exact meaning of each depends on the extension class. See their documentation.
Before 11.0.0 this attribute (target or language) was required to
be defined even before the initialization of the extension. If an
extension class didn’t have it, it was just skipped by byexample
Since 11.0.0 such attribute could not exist before the initialization
but it must exist after and byexample will complain if it isn’t.
>>> class BadTarget(Concern):
... def __init__(self, **kargs):
... Concern.__init__(self, **kargs)
...
... # XXX 'target' attribute is missing,
... # byexample will complain about this
... assert not hasattr(self, 'target')
$ byexample -m test/ds/bad/target/missing/ -l python --dry docs/languages/python.md # byexample: +norm-ws
[!] Something went wrong initializing byexample:
From '<...>test/ds/bad/target/missing' module 'bad'
The object of class BadTarget did not define a 'target' attribute.
<...>
byexample also will check the type of target / languages.
Depending on the extension class that you are extending, the type must be
a string (single-valued) or a list-like strings (multi-valued).
For a Concern for example it must be a string: byexample will complain
if it is set to other thing:
>>> class BadTarget(Concern):
... # XXX This is wrong, a target cannot be a list.
... target = ['bogusmodule']
...
... def __init__(self, **kargs):
... Concern.__init__(self, **kargs)
$ byexample -m test/ds/bad/target/invalid/ -l python --dry docs/languages/python.md # byexample: +norm-ws
[!] Something went wrong initializing byexample:
From '<...>test/ds/bad/target/invalid' module 'bad'
The attribute 'target' of BadTarget must be a single string-like value but it is of type <class 'list'>.
<...>
New in
byexample 11.0.0: before11.0.0, a class was ignored bybyexampleif it didn’t have atarget/languageattribute even if the class inherited from an extension class likeExampleParserorConcern. Since11.0.0, any class the inherit from an extension class will be loaded and itstarget/languagewill be checked after the initialization and if an error is found,byexamplewill make it explicit.
For ZoneDelimiter, its target can be multivalued, like a list or
set. Empty targets or with duplicated are not considered errors but a
warning is issued.
>>> class MultiTargetDuplicated(ZoneDelimiter):
... # This is not an error but clearly a typo.
... target = ['foo', 'foo']
>>> class MultiTargetEmpty(ZoneDelimiter):
... # This is not an error but setting to None is better
... # to make explicit the intention
... target = []
$ byexample -m test/ds/bad/target/multi/ -l python --dry docs/languages/python.md # byexample: +norm-ws
[w] Extension MultiTargetDuplicated has duplicated entries in its
'target' attribute.
[w] Extension MultiTargetEmpty has no entries in its 'target' attribute.
If is intentional, prefer to set None instead.
Disabling an extension dynamically
An extension can disable itself by setting its target / language to
None during its initialization (__init__ call).
This is handy because __init__ will receive the configuration (cfg
parameter) and the extension will have the opportunity to check flags
and options (cfg.options) and decide if it should run or not.