Git Product home page Git Product logo

python-pyfields's Introduction

python-pyfields

Define fields in python classes. Easily.

Python versions Build Status Tests Status Coverage Status codecov Flake8 Status

Documentation PyPI Downloads Downloads per week GitHub stars

This is the readme for developers. The documentation for users is available here: https://smarie.github.io/python-pyfields/

Want to contribute ?

Contributions are welcome ! Simply fork this project on github, commit your contributions, and create pull requests.

Here is a non-exhaustive list of interesting open topics: https://github.com/smarie/python-pyfields/issues

nox setup

This project uses nox to define all lifecycle tasks. In order to be able to run those tasks, you should create python 3.7 environment and install the requirements:

>>> conda create -n noxenv python="3.7"
>>> activate noxenv
(noxenv) >>> pip install -r noxfile-requirements.txt

You should then be able to list all available tasks using:

>>> nox --list
Sessions defined in <path>\noxfile.py:

* tests-2.7 -> Run the test suite, including test reports generation and coverage reports.
* tests-3.5 -> Run the test suite, including test reports generation and coverage reports.
* tests-3.6 -> Run the test suite, including test reports generation and coverage reports.
* tests-3.8 -> Run the test suite, including test reports generation and coverage reports.
* tests-3.7 -> Run the test suite, including test reports generation and coverage reports.
- docs-3.7 -> Generates the doc and serves it on a local http server. Pass '-- build' to build statically instead.
- publish-3.7 -> Deploy the docs+reports on github pages. Note: this rebuilds the docs
- release-3.7 -> Create a release on github corresponding to the latest tag

Running the tests and generating the reports

This project uses pytest so running pytest at the root folder will execute all tests on current environment. However it is a bit cumbersome to manage all requirements by hand ; it is easier to use nox to run pytest on all supported python environments with the correct package requirements:

nox

Tests and coverage reports are automatically generated under ./docs/reports for one of the sessions (tests-3.7).

If you wish to execute tests on a specific environment, use explicit session names, e.g. nox -s tests-3.6.

Editing the documentation

This project uses mkdocs to generate its documentation page. Therefore building a local copy of the doc page may be done using mkdocs build -f docs/mkdocs.yml. However once again things are easier with nox. You can easily build and serve locally a version of the documentation site using:

>>> nox -s docs
nox > Running session docs-3.7
nox > Creating conda env in .nox\docs-3-7 with python=3.7
nox > [docs] Installing requirements with pip: ['mkdocs-material', 'mkdocs', 'pymdown-extensions', 'pygments']
nox > python -m pip install mkdocs-material mkdocs pymdown-extensions pygments
nox > mkdocs serve -f ./docs/mkdocs.yml
INFO    -  Building documentation...
INFO    -  Cleaning site directory
INFO    -  The following pages exist in the docs directory, but are not included in the "nav" configuration:
  - long_description.md
INFO    -  Documentation built in 1.07 seconds
INFO    -  Serving on http://127.0.0.1:8000
INFO    -  Start watching changes
...

While this is running, you can edit the files under ./docs/ and browse the automatically refreshed documentation at the local http://127.0.0.1:8000 page.

Once you are done, simply hit <CTRL+C> to stop the session.

Publishing the documentation (including tests and coverage reports) is done automatically by the continuous integration engine, using the nox -s publish session, this is not needed for local development.

Packaging

This project uses setuptools_scm to synchronise the version number. Therefore the following command should be used for development snapshots as well as official releases: python setup.py sdist bdist_wheel. However this is not generally needed since the continuous integration engine does it automatically for us on git tags. For reference, this is done in the nox -s release session.

Merging pull requests with edits - memo

Ax explained in github ('get commandline instructions'):

git checkout -b <git_name>-<feature_branch> master
git pull https://github.com/<git_name>/python-pyfields.git <feature_branch> --no-commit --ff-only

if the second step does not work, do a normal auto-merge (do not use rebase!):

git pull https://github.com/<git_name>/python-pyfields.git <feature_branch> --no-commit

Finally review the changes, possibly perform some modifications, and commit.

python-pyfields's People

Contributors

smarie avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

python-pyfields's Issues

Fast route without trying converters

From #5

matching converters will be called even if received value is of a valid type, except if a specific option skip_matching_converters_if_valid=True is set

How to do equality comparision of objects when properties are lazily defined

I'm facing an issue while using pyfields, it has to do with the fact that properties are lazily initialized. Some of my tests require doing an equality comparison of two objects.
Let's say I have the following class:

class A:
     field1: str = field(check_type=True, default=None)
     
    def __eq__(self, other):
        return type(self) is type(other) and self.__dict__ == other.__dict__

Now,

a1 = A()
a1.field1

a2 = A()

assert a1 == a2

Here, a1 !=a2 since calling a1.field would set a _field variable inside the instance.

This is not exactly a bug, but can you think of any clean way of handling this with pyfields?

Nonable fields don't work with type validations

If I have a Nonable field, setting it to None, or calling a constructor with the field unspecified should not raise a validation error. However, this happens if I have typegaurd installed

Example code to reproduce:

class A:
     f: str = field(check_type=True, default=None, nonable=True)
     @init_fields
     def __init__():
            pass

a = A()

This results in the following traceback:

raceback (most recent call last):
  File "/Users/devashishshankar/Work/ddp_v1_venv/lib/python3.7/site-packages/pyfields/typing_utils.py", line 58, in assert_is_of_type
    check_type(field.qualname, value, typ)
  File "/Users/devashishshankar/Work/ddp_v1_venv/lib/python3.7/site-packages/typeguard/__init__.py", line 544, in check_type
    format(argname, qualified_name(expected_type), qualified_name(value)))
TypeError: type of A.f must be str; got NoneType instead

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<makefun-gen-36>", line 2, in __init__
  File "/Users/devashishshankar/Work/ddp_v1_venv/lib/python3.7/site-packages/pyfields/init_makers.py", line 446, in __init__
    setattr(self, field_name, field_value)
  File "/Users/devashishshankar/Work/ddp_v1_venv/lib/python3.7/site-packages/pyfields/core.py", line 1066, in __set__
    assert_is_of_type(self, value, t)
  File "/Users/devashishshankar/Work/ddp_v1_venv/lib/python3.7/site-packages/pyfields/typing_utils.py", line 63, in assert_is_of_type
    raise new_e
pyfields.typing_utils.FieldTypeError: Invalid value type provided for 'A.f'. Value should be of type <class 'str'>. Instead, received a 'NoneType': None

Generated init methods should or could check that all mandatory fields in the class have been set.

From #2

At the end of generated init methods, we could optionally enable validation that all mandatory fields have been set. That could be implicit or explicit. A prerequisite is to have a function able to provide this functionality on an object class (getting behind the scenes the list of fields applicable to this class).

This function could also be explicitly used by users, anywhere in their code (in an init method or elsewhere)

Note that this is not as simple as looking at the init arguments

`make_init`

feature 5

user wants fully automatic init creation

5a with decorator

@auto_init('height')
class Wall(object):
    height: int = field(doc="Height of the wall in mm.")
    color: str = field(default='white', doc="Color of the wall.")

pros

  • a simple decorator, and "all fields" is easy to declare: decorator without arguments.

cons

  • fields are referenced by name: no autocompletion if a particular selection needs to be made.
  • decorator = a bit harder to debug and can be frightening

5b with decorator but fields declare if they are in init

@auto_init
class Wall(object):
    height: int = field(doc="Height of the wall in mm.", in_init=True)
    color: str = field(default='white', doc="Color of the wall.", in_init=False)

pros

  • more compact decoration

cons

  • I do not like the fact that the inclusion in init is a property of the fields. This will cause trouble in inheritance and mixin scenarii.

5c explicit method

class Wall(object):
    height: int = field(doc="Height of the wall in mm.")
    color: str = field(default='white', doc="Color of the wall.")
    __init__ = make_init(height, color)

pros

  • compact
  • autocompletion works if an explicit list or order needs to be provided
  • this can probably be smoothly extended to support scenario 4 as well (a __post_init__ method or even any method using make_init(..., post=<method_post>) or make_init(color, my_post_init_method, height) so as to control precisely which part of the signature will go where)

I do not see much cons here

Originally posted by @smarie in #2 (comment)

Support 'converter' in field definitions

A few requirements/ideas:

  • both as argument and as decorator. both can be specified. Decorator is @<field_name>.converter(*types) will be handled in #28
  • if argument: a possibly ordered dictionary of type: converter or (*types): converter.
    - For old python support, a list of key/val tuples is supported.
    - '*': <> can be used to indicate "the rest"
    - None should be used for the none type
    - Is(x) for constant-only converters
  • if argument: a single converter can be passed, in which case it will be considered '*'
  • if several converters match, they will be tried in order until no exception is raised / no CONVERSION_FAILED is returned (returning None is considered a successful conversion)
  • matching converters will be called even if received value is of a valid type, except if a specific option skip_matching_converters_if_valid=True is set now in #30
  • by default after conversion, validation will happen as always. A validate_after_converting=False argument or converter option would also be needed to change this behaviour. now in #29

`check_type` does not work with Subscripted generic fields

If I have a field with type_hint being something like List[A], and check_type=True, I would expect it to validate the field. However, the code throws the following error on class init:

TypeError: Subscripted generics cannot be used with class and instance checks

This seems to be the default behavior of isinstance when dealing with any subscripted generic field, for e.g. List[str] or Optional[str]. See related SO answer. Consequently validate also suffers with the same issue.

There is an easy workaround, setting check_type=False, and adding a custom validator:

f = field(type_hint=List[A], check_type=False,
              validators=lambda l: isinstance(l, list) and all([isinstance(e, A) for e in l]),)

However, I still thought I would add this issue in case someone runs into it. (Don't know if this should be handled at a library level, given this is a limitation of the default isinstance)

Support `validator` in `field()` definitions

Spec:

  • both as field() argument and as @<field>.validator(...) decorator. Both can be specified in which case the arguments are used first.
  • if argument: a possibly ordered dictionary of <error_msg>: <validator>. For old python support, a list of key/val tuples is supported.
  • <validator> should be a callable accepting one, two or three arguments (val), (obj, val) or (obj, field, val) so that it has access to the full object on value modification (it can check values of other fields)
  • <validator> should return True or None in case of success (same definition than valid8)
  • when provided as a decorator, the error message is the argument of the decorator: @<field_name>.validator(<error message>)

Installation fails on ubuntu python3

Steps to reproduce (on docker):

docker pull ubuntu
docker run -it ubuntu bash

# Inside docker shell
apt-get -y update
apt-get install -y python python3 python-pip python3-pip
pip3 install pyfields

Stacktrace:

root@eddc884167cf:/# pip3 install pyfields
Collecting pyfields
  Downloading https://files.pythonhosted.org/packages/9f/4b/1e3eb1b5f35f51117d8b9801a4c051bdbc1b5d9889d1b48cdb056b1f7ec4/pyfields-0.12.0-py3-none-any.whl (43kB)
    100% |################################| 51kB 140kB/s
Collecting sentinel (from pyfields)
  Downloading https://files.pythonhosted.org/packages/2e/12/867f97b68c2a541f1e0de6b3b8ff52a2c2bef8d88228efc885ad07f20968/sentinel-0.1.1.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-build-4q0121fl/sentinel/setup.py", line 5, in <module>
        long_description = readme.read()
      File "/usr/lib/python3.6/encodings/ascii.py", line 26, in decode
        return codecs.ascii_decode(input, self.errors)[0]
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xc2 in position 64: ordinal not in range(128)

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-build-4q0121fl/sentinel/

Nonable fields

From #43 :

As of now we seem to have to explicitly declare optionality for the "check type" to skip the type validation in case of a nullable field.

For example:

from typing import Optional

class A:
     field1: Optional[int] = field(check_type=True, default=None)
     field2: Optional[int] = field(check_type=True)

Note that this is completely independent from the fact that a field is optional or mandatory. However the usual python behaviour for typing is to consider nullable all arguments that have a None default value, see https://stackoverflow.com/a/57390124/7262247.

Support global checker

if the class contains a __validate_all_fields__(self) method, it will be called everytime a field is modified. This is quite heavy but might be useful in some rare cases where validation can not be done in each of the fields validators

  • you can disable this temporarily by doing "with skip_global_validation(o): ..." (implementation note: this would set a __skip_global_validation__ flag on the instance, read by the fields setters)

  • or permanently with set_global_validation(o, enabled=False) ?

The `__init__` topic

Several cases to support

1. user writes his own init entirely. This is already supported and he can set the fields correctly (or not, for the optional)
2. user writes his own entirely but not the type hints, doc and defaults (they have already been declared, why copying again). Support the case where he uses all, or some of, the fields as constructor arguments. (will not be fixed for now)
3. user writes his own but does not want to write all the argument names so he could use a special variable fields in the constructor, and anywhere in his constructor do fields.assign() or fields.init(). (fixed in #13 - @inject_fields)
4. user writes his own but wished object creation to be faster than the above. In that case (to benchmark though), we would be rather interested by some kind of a __post_init__ support like in @dataclass (fixed in 0.5.0 - @init_fields and make_init)
5. user wants fully automatic init creation (fixed in 0.5.0 - make_init)

In all these cases,

  • The initialization order can be by default the order of appearance in the class. We could also add a init_after=<fields> argument to field() so that a field requiring other fields to be set before validation, converter or default_factory execution could be initialized afterwards. Another option would be to support a special way to declare that one of these three arguments depends on another field. (now in #23)
  • we could optionally enable validation that all mandatory fields have been set at the end of init. That could be implicit or explicit (i.e. in use case 3. above, that could be done in fields.assign ?) (now in #24)

Support explicit `init_after` constraints so that fields can be initialized in another order than the default

The initialization order can be by default the order of appearance in the class. We could also add a init_after=<fields> argument to field() so that a field requiring other fields to be set before validation, converter or default_factory execution could be initialized afterwards. Another option would be to support a special way to declare that one of these three arguments depends on another field.

How to specify superclass field order in `init_fields` or `make_init`?

Let's say I have classes:

class A:
     f1 = field()
     __init__ = make_init()

class B(A):
     f2 = field()
     __init__ = make_init(f2, A.f1)

i.e. I want to change the order of arguments in init. The above fails with the error:

pyfields.core.ClassFieldAccessError: Accessing a `field` from the class is not yet supported. See https://github.com/smarie/python-pyfields/issues/12

Any way to make this work? Till then I'm working around by manually creating the __init__

`@inject_fields` decorator for `__init__`

Feature 3

user writes his own but does not want to write all the argument names so he could use a special variable fields in the constructor, and anywhere in his constructor do fields.assign() or fields.init().

        @inject_fields(height, color)
        def __init__(self, fields):
            fields.init()

pros:

  • you can use autocompletion so it is fast to type the decorators
  • you only declare the fields that you need, in the order that you like
  • you control when init happens
  • you can easily debug what happens during field initialization

cons:

  • it does not seem possible to have @inject_fields work without arguments, or even with the class as argument, to say "use all fields from class", except if we allow introspection to be used. Indeed, at the time where the __init__ method is decorated, it is not yet bound to the class. EDIT actually it is possible: we can create a non-data descriptor on the class (this was inspired by this link).

Originally posted by @smarie in #2 (comment)

Capability to register listeners (observers) on read and write

We need to clarify

  • the callback signature: probably just (obj, field) for get and (obj, field, value, has_changed) for set.
  • should the observer be called in the same thread ? probably yes for now. Later we could imagine using asyncio.loop.call_soon but that seems overkill for now
  • should the observer be called after setting the value in case of a set observer ? Maybe better to support (A) generators with a single yield and (B) normal methods, in which case they will be called after* the field is set.
  • the name of the option in field() and in the associated decorator (like for validators and converters). Maybe get_observers and set_observers (without the 's' for decorator and corresponding add_xxx method) ?

See https://traitlets.readthedocs.io/en/stable/using_traitlets.html#observe and https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Observer.html

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.