-
Notifications
You must be signed in to change notification settings - Fork 26
[WIP] Specify by Attributes #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e5e72c5
e1ab33f
14451ba
6be78e7
3ef954d
f18ef90
892599f
079b916
0881c67
1d1f2f3
02331cb
b317ce8
a4969f4
72407f0
6c0dc94
8323784
e5000b1
8c17e65
d1b3950
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| PCIMI Developer Documentation | ||
| ============================= | ||
|
|
||
| This section is aimed at the developer of PICMI and guide them through general design and internal functionality of PICMI itself, i.e. the python module ``picmistandard``. | ||
| It does **not** explain how to implement PICMI in an existing PIC simulation. | ||
|
|
||
| .. toctree:: | ||
| :maxdepth: 1 | ||
|
|
||
| general_class_design |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| General Class Design | ||
| ==================== | ||
|
|
||
| All PICMI classes inherit from ``_ClassWithInit``, which makes a class defined by purely attributes exhibit *the typical PICMI behavior*: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| >>> class MyClass(_ClassWithInit): | ||
| ... mandatory_attr: typing.Any | ||
| ... name: str = "" | ||
| ... optional: typing.Optional[int] = 3 | ||
| ... _protected = 1 | ||
| >>> my_object = MyClass(mandatory_attr=[], | ||
| ... name="any string") | ||
| >>> [] == my_object.mandatory_attr | ||
| True | ||
| >>> "any string" == my_object.name | ||
| True | ||
| >>> 3 == my_object.optional | ||
| True | ||
| >>> my_object.optional = None | ||
| >>> my_object.check() # <- silently passes | ||
| >>> my_object.optional = "not an integer" | ||
| >>> my_object.check() # <- now throws | ||
| [...TypeError stack trace omitted...] | ||
|
|
||
|
|
||
| General Functionality | ||
| --------------------- | ||
| When a class has a set attributes, which may have a `PEP 484 type annotation <https://peps.python.org/pep-0484/>`_, and a default value, a constructor is provided which behaves as follows: | ||
|
|
||
| For all arguments (``kwargs``) check: | ||
|
|
||
| - **allow** if: | ||
| - attribute is defined, or | ||
| - attribute starts with the current code name, or | ||
| - attribute starts with a known code name | ||
| - **reject** if: | ||
| - attribute starts with a ``_`` (is private or protected), or | ||
| - is unkown (and not one of the exceptions above) | ||
|
|
||
| If a type annotation is given, the value is checked against it, otherwise all types are allowed. | ||
|
|
||
| If a type has **no default value** it is considered **mandatory**: | ||
| The constructure will abort operation if it is not given. | ||
|
|
||
| Additionally, an implementing class may implement a method ``_check(self) -> None``, which can perform additional checks. | ||
|
|
||
| All checks (type checks and custom checks by ``_check()``) can be manually invoked by calling ``check()``. | ||
|
|
||
| After construction ``check()`` will automatically be called. | ||
|
|
||
| An implementation should call ``check()`` just before it begins it operation too. | ||
|
|
||
| Mandatory and Optional Attributes | ||
| --------------------------------- | ||
|
|
||
| Unless a default value is given, attributes are considered mandatory. | ||
| Note that the default must conform to the given type specification -- to set a default to ``None``, the type specification must allow it. | ||
|
|
||
| Type Checking | ||
| ------------- | ||
|
|
||
| Type checking is performed only for attributes, and delegated to the library `typeguard <https://typeguard.readthedocs.io/en/latest/>`_. | ||
| Methods must be checked by other means (e.g. by using typeguard ``@typechecked`` annotation). | ||
|
|
||
| Other Checks | ||
| ------------ | ||
|
|
||
| The method ``check()`` provides type checking for the constructor and other objects. | ||
|
|
||
| - ``check()``: **external** interface, performs typechecks and calls ``_check()``. **DO NOT OVERWRITE** | ||
| - ``_check()``: **custom** hook, optional, automatically called from wrapper ``check()``. **DO OVERWRITE** | ||
|
|
||
| To implement custom checks overwrite ``_check(self) -> None``, which will be called from the wrapper ``check()`` after all typechecks have passed. | ||
|
|
||
| ``_check()`` must raise when it encounters an error, passing silently will consider the check to have succeeded. | ||
|
|
||
| Note that owned objects will not be checked by default, consider calling their respective ``check()`` inside the custom ``_check()``. | ||
|
|
||
| Full Reference | ||
| -------------- | ||
|
|
||
| .. autoclass:: picmistandard.base._ClassWithInit | ||
| :members: __init__, _check, check |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| """base code for the PICMI standard | ||
| """ | ||
|
|
||
| import typing | ||
| import typeguard | ||
|
|
||
| codename = None | ||
|
|
||
| # --- The list of supported codes is needed to allow checking for bad arguments. | ||
|
|
@@ -24,30 +27,159 @@ def register_constants(implementation_constants): | |
| def _get_constants(): | ||
| return _implementation_constants | ||
|
|
||
| class _ClassWithInit(object): | ||
| def handle_init(self, kw): | ||
| # --- Grab all keywords for the current code. | ||
| # --- Arguments for other supported codes are ignored. | ||
| # --- If there is anything left over, it is an error. | ||
| codekw = {} | ||
| for k,v in kw.copy().items(): | ||
| code = k.split('_')[0] | ||
| if code == codename: | ||
| codekw[k] = v | ||
| kw.pop(k) | ||
| elif code in supported_codes: | ||
| kw.pop(k) | ||
|
|
||
| if kw: | ||
| raise TypeError('Unexpected keyword argument: "%s"'%list(kw)) | ||
|
|
||
| # --- It is expected that init strips accepted keywords from codekw. | ||
| self.init(codekw) | ||
|
|
||
| if codekw: | ||
| raise TypeError("Unexpected keyword argument for %s: '%s'"%(codename, list(codekw))) | ||
|
|
||
| def init(self, kw): | ||
| # --- The implementation of this routine should use kw.pop() to retrieve input arguments from kw. | ||
| # --- This allows testing for any unused arguments and raising an error if found. | ||
|
|
||
| class _ClassWithInit: | ||
| """ | ||
| Use args from constructor as attributes | ||
|
|
||
| Non-given attributes are left untouched (i.e. at their default). | ||
|
|
||
| Attributes can be marked as mandatory by adding a type annotation. | ||
| Type annotations are enforced by check(), i.e. normal assignments for | ||
| attributes with wrong types still work. | ||
|
|
||
| Arguments that are prefixed with codename and underscore _ will be | ||
| accepted, as well as equivalent prefixes for other supported codes. | ||
| All other arguments will be rejected. | ||
| """ | ||
|
|
||
| def __check_arg_valid(self, arg_name: str) -> None: | ||
| """ | ||
| check if arg_name is acceptable for attr | ||
|
|
||
| If ok silently pass, else raise. | ||
|
|
||
| An arg is valid if: | ||
| - has a default | ||
| - has a type annotation (i.e. is mandatory) | ||
| - is prefixed with codename and underscore _ | ||
| - is prefixed with any supported codename and underscore _ | ||
| - is equal to codename/any supported codename | ||
| - does *NOT* begin with underscore _ | ||
| """ | ||
| assert codename is not None | ||
| self_type = type(self) | ||
|
|
||
| if arg_name.startswith("_"): | ||
| raise NameError( | ||
| f"protected/private attribute may NOT be accessed: {arg_name}") | ||
|
|
||
| if arg_name in self_type.__dict__: | ||
| # has default value i.e. is defined | ||
| return | ||
|
|
||
| if arg_name in typing.get_type_hints(self_type): | ||
| # mandatory arg (has no default) | ||
| return | ||
|
|
||
| # check prefix: | ||
| prefix = arg_name.split("_")[0] | ||
| if prefix == codename: | ||
| return | ||
|
|
||
| if prefix in supported_codes: | ||
| return | ||
|
|
||
| # arg name is not in allowed sets -> raise | ||
| raise NameError(f"unkown argument: {arg_name}") | ||
|
|
||
| def __get_mandatory_attrs(self) -> typing.Set[str]: | ||
| """ | ||
| Retrieve list of mandatory attrs | ||
|
|
||
| Attributes are considered mandatory if they exist (which they | ||
| syntactically only can if they have a *type annotation*), but no | ||
| default value. | ||
| """ | ||
| self_type = type(self) | ||
| has_type_annotion = typing.get_type_hints(type(self)).keys() | ||
|
|
||
| # ignore those with default values | ||
| return set(has_type_annotion - self_type.__dict__.keys()) | ||
|
|
||
| def check(self) -> None: | ||
| """ | ||
| checks self, raises on error, passes silently if okay | ||
|
|
||
| ! MUST NOT BE OVERWRITTEN ! | ||
| -> overwrite _check() (note the leading _) instead | ||
|
|
||
| Performs the following checks: | ||
| 1. The type of all attributes are checked against their type | ||
| annotations (if present). On error a TypeError is raised. | ||
| 2. If (and only if) the typechecks passed the custom-overwriteable | ||
| method _check() is called. | ||
|
|
||
| Will be called inside of __init__(), and should be called before any | ||
| work on the data is performed. | ||
| """ | ||
| for arg_name, annotation in typing.get_type_hints(type(self)).items(): | ||
| # perform type check: will raise on error | ||
| typeguard.check_type(arg_name, | ||
| getattr(self, arg_name), | ||
| annotation) | ||
|
|
||
| self._check() | ||
|
|
||
| def __check_types_of_defaults(self) -> None: | ||
| """ | ||
| run typechecks for defaults | ||
|
|
||
| Ensures that defaults for attributes conform to their type annotations. | ||
| Passes silently if okay, else raises type error. | ||
| """ | ||
| for arg_name, annotation in typing.get_type_hints(type(self)).items(): | ||
| if arg_name not in type(self).__dict__: | ||
| # there is no default -> skip loop iteration | ||
| continue | ||
| default_value = type(self).__dict__[arg_name] | ||
| # perform type check: will raise on error | ||
| try: | ||
| typeguard.check_type(arg_name, default_value, annotation) | ||
| except TypeError: | ||
| # replace by custom message | ||
| actual_type = type(default_value) | ||
| raise TypeError(f"default value for {arg_name} must be of " | ||
| "type {annotation}, but got {actual_type}") | ||
|
|
||
| def _check(self) -> None: | ||
| """ | ||
| run checks that are not typechecks | ||
|
|
||
| Should be overwritten by child class. | ||
|
|
||
| Will be called from check(), and thereby from __init__(). | ||
|
|
||
| When this method passes it guarantees that self is conforming to PICMI. | ||
| This includes all children (should there be any) -- their check() | ||
| method should be called from this method. | ||
|
|
||
| When it is not overwritten, it is replaced by this empty parent method. | ||
| """ | ||
| # parent implementation: just pass | ||
| pass | ||
|
|
||
| def __init__(self, **kw): | ||
| """ | ||
| parse kw and set class attributes accordingly | ||
|
|
||
| See class docstring for detailed description. | ||
|
|
||
| ! MUST NOT BE OVERWRITTEN ! | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: hard to enforce, maybe even error-prune API contract and maybe not ideal for something as common as |
||
| (Constructors MUST NOT exhibit unpredictable behavior==behavior | ||
| different from the one specified here.) | ||
| """ | ||
| mandatory_missing = self.__get_mandatory_attrs() - kw.keys() | ||
| if 0 != len(mandatory_missing): | ||
| raise RuntimeError( | ||
| "mandatory attributes are missing: {}" | ||
| .format(", ".join(mandatory_missing))) | ||
|
|
||
| self.__check_types_of_defaults() | ||
|
|
||
| for name, value in kw.items(): | ||
| self.__check_arg_valid(name) | ||
| setattr(self, name, value) | ||
|
|
||
| # perform self-check -> will alert for invalid params | ||
| self.check() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| numpy~=1.15 | ||
| scipy~=1.5 | ||
| typeguard | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checked: pure python (incl. dependencies) & easy to install it seems |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Testing | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for adding unit tests. Definitely something this repo needs. |
||
| PICMI is currently not thoroughly tested on its own. | ||
| Feel free to contribute! | ||
|
|
||
| [python unittest docs](https://docs.python.org/3.8/library/unittest.html) | ||
|
|
||
| ## Path Workaround | ||
| To allow `import picmistandard` to refer to the actual source of this repository, | ||
| this directory contains a symbolic link named `picmistandard` to the actual source. | ||
|
|
||
| By supplying an appropriate `PYTHONPATH` this module is loaded. | ||
| (The current directory is always available for imports, so this is not necessarily required.) | ||
|
|
||
| ## Unittests | ||
| Unittests are launched from the `__main__` function from the unittest directory. | ||
| This tests the currently available module `picmistandard`. | ||
|
|
||
| The file structure follows the source 1-to-1. | ||
|
|
||
| To test the development version run (from this directory): | ||
|
|
||
| ``` | ||
| python -m unit | ||
| ``` | ||
|
|
||
| ## E2E | ||
| Execute the example as end-to-end test by launching `./launch_e2e_test.sh` from this directory. | ||
| Note that it requires the python module `fbpic` to be available. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../PICMI_Python |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from .base import * |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| from . import * | ||
|
|
||
| import unittest | ||
| unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: hard to enforce API contract