diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..8c2a43d7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 10 +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: monthly diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 00000000..89ad4557 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,98 @@ +name: build +on: + push: + branches: ["main"] + tags: ["*"] + pull_request: + +jobs: + docs: + name: docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + - run: pip install tox + - run: tox -e docs + tests: + name: ${{ matrix.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { name: "3.10-pymongo3", python: "3.10", tox: py310-pymongo3 } + - { name: "3.13-pymongo4", python: "3.13", tox: py313-pymongo4 } + - { name: "3.10-motor", python: "3.10", tox: py310-motor } + - { name: "3.13-motor", python: "3.13", tox: py313-motor } + - { name: "3.10-txmongo", python: "3.10", tox: py310-txmongo } + - { name: "3.13-txmongo", python: "3.13", tox: py313-txmongo } + steps: + - uses: actions/checkout@v6 + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.12.1 + with: + mongodb-version: 8.0 + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + - run: python -m pip install tox + - run: python -m tox -e ${{ matrix.tox }} + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + build: + name: Build package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install pypa/build + run: python -m pip install build + - name: Build a binary wheel and a source tarball + run: python -m build + - name: Install twine + run: python -m pip install twine + - name: Check build + run: python -m twine check --strict dist/* + - name: Store the distribution packages + uses: actions/upload-artifact@v5 + with: + name: python-package-distributions + path: dist/ + # this duplicates pre-commit.ci, so only run it on tags + # it guarantees that linting is passing prior to a release + lint-pre-release: + if: startsWith(github.ref, 'refs/tags') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + - run: python -m pip install tox + - run: python -m tox -e lint + publish-to-pypi: + name: PyPI release + if: startsWith(github.ref, 'refs/tags/') + needs: [lint-pre-release, build, tests] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/umongo + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v6 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..981767a7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +ci: + autoupdate_schedule: monthly +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.7 + hooks: + - id: ruff-check + - id: ruff-format +- repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.35.0 + hooks: + - id: check-github-workflows + - id: check-readthedocs diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..fd42639b --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,15 @@ +version: 2 +sphinx: + configuration: docs/conf.py +formats: + - pdf +build: + os: ubuntu-22.04 + tools: + python: "3.13" +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/AUTHORS.rst b/AUTHORS.rst index e78054a0..2b22487c 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -26,3 +26,4 @@ Contributors * Attila Kóbor `@atti92 `_ * Denis Moskalets `@denya `_ * Phil Chiu `@whophil `_ +* sanju sci `@sanjusci `_ diff --git a/HISTORY.rst b/CHANGELOG.rst similarity index 94% rename from HISTORY.rst rename to CHANGELOG.rst index 3d665110..ad021675 100644 --- a/HISTORY.rst +++ b/CHANGELOG.rst @@ -1,6 +1,50 @@ -======= -History -======= +========= +Changelog +========= + +4.0.0b4 (2025-10-07) +-------------------- + +Features: + +* *Backwards-incompatible*: ``find_one`` methods don't accept ``*args`` anymore (#411). + +4.0.0b3 (2025-10-02) +-------------------- + +Other: + +* Drop marshmallow 3 (#408) + +4.0.0b2 (2025-09-22) +-------------------- + +Other: + +* Drop motor 2 (#406) +* Require Python 3.10 (#406) + +4.0.0b1 (2025-09-21) +-------------------- + +Features: + +* Support pymongo 4 (#392) +* Support motor 3 (#392) +* Support marshmallow 4 (#400) +* *Backwards-incompatible*: ``missing`` and ``default`` attributes are no longer + used in umongo fields, only ``dump_default`` and ``load_default`` are used. + ``marshmallow_load_default`` and ``marshmallow_dump_default`` attributes may + be used to overwrite the values to use in the pure marshmallow field returned + by ``as_marshmallow_field`` method. (#392) + +Other: + +* *Backwards-incompatible*: Remove ``__version__``, ``__author__`` and + ``__email__`` from umongo.__init__.py (#395). +* Require marshmallow >=3.26 (#401) +* Support Python up to 3.13 (#392) +* Drop Python 3.7 and 3.8 (#393) 3.1.0 (2021-12-23) ------------------ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5d9d3f86..63c27bb1 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -60,6 +60,7 @@ Get Started! Ready to contribute? Here's how to set up `umongo` for local development. 1. Fork the `umongo` repo on GitHub. + 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/umongo.git @@ -68,51 +69,30 @@ Ready to contribute? Here's how to set up `umongo` for local development. $ mkvirtualenv umongo $ cd umongo/ - $ python setup.py develop + $ pip install -e .[dev] -4. Create a branch for local development:: +4. Install the pre-commit hooks, which will format and lint your git staged files:: - $ git checkout -b name-of-your-bugfix-or-feature + $ pre-commit install - Now you can make your changes locally. +5. Create a branch for local development:: -5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: + $ git checkout -b name-of-your-bugfix-or-feature - $ flake8 umongo - $ py.test tests - $ tox + Now you can make your changes locally. Please add necessary feature or non-regression tests. - To get flake8, pytest and tox, just pip install them into your virtualenv. +5. When you're done making changes, check that your changes pass tests:: -.. note:: You need pytest>=2.8 + $ pytest 6. Commit your changes and push your branch to GitHub:: $ git add . - $ git commit -m "Your detailed description of your changes." - $ git push origin name-of-your-bugfix-or-feature + $ git commit -m "Detailed description of your changes." + $ git push -u origin name-of-your-bugfix-or-feature 7. Submit a pull request through the GitHub website. -I18n ----- - -There are additional steps to make changes involving translated strings. - -1. Extract translatable strings from the code into messages.pot:: - - $ make extract_messages - -2. Update flask example translation files:: - - $ make update_flask_example_messages - -3. Update/fix translations - -4. Compile new binary translation files:: - - $ make compile_flask_example_messages - Pull Request Guidelines ----------------------- @@ -122,6 +102,3 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. -3. The pull request should work for Python 3.7 and 3.8. Check - https://travis-ci.org/touilleMan/umongo/pull_requests - and make sure that the tests pass for all supported Python versions. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 2bb6bb1c..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -include AUTHORS.rst -include CONTRIBUTING.rst -include HISTORY.rst -include LICENSE -include README.rst - -recursive-include tests * -recursive-exclude * __pycache__ -recursive-exclude * *.py[co] - -recursive-include docs *.rst conf.py Makefile make.bat diff --git a/Makefile b/Makefile deleted file mode 100644 index 592ea69f..00000000 --- a/Makefile +++ /dev/null @@ -1,99 +0,0 @@ -.PHONY: clean-pyc clean-build docs clean -define BROWSER_PYSCRIPT -import os, webbrowser, sys -try: - from urllib import pathname2url -except: - from urllib.request import pathname2url - -webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) -endef -export BROWSER_PYSCRIPT -BROWSER := python -c "$$BROWSER_PYSCRIPT" - -help: - @echo "clean - remove all build, test, coverage and Python artifacts" - @echo "clean-build - remove build artifacts" - @echo "clean-pyc - remove Python file artifacts" - @echo "clean-test - remove test and coverage artifacts" - @echo "lint - check style with flake8" - @echo "test - run tests quickly with the default Python" - @echo "test-all - run tests on every Python version with tox" - @echo "coverage - check code coverage quickly with the default Python" - @echo "docs - generate Sphinx HTML documentation, including API docs" - @echo "release - package and upload a release" - @echo "dist - package" - @echo "install - install the package to the active Python's site-packages" - -clean: clean-build clean-pyc clean-test - -clean-build: - rm -fr build/ - rm -fr dist/ - rm -fr .eggs/ - find . -name '*.egg-info' -exec rm -fr {} + - find . -name '*.egg' -exec rm -f {} + - -clean-pyc: - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - find . -name '__pycache__' -exec rm -fr {} + - -clean-test: - rm -fr .tox/ - rm -f .coverage - rm -fr htmlcov/ - -lint: - flake8 umongo tests - -test: - python setup.py test - -test-all: - tox - -coverage: - coverage run --source umongo setup.py test - coverage report -m - coverage html - $(BROWSER) htmlcov/index.html - -docs: - rm -f docs/umongo.rst - rm -f docs/modules.rst - sphinx-apidoc -o docs/ umongo - $(MAKE) -C docs clean - $(MAKE) -C docs html - $(BROWSER) docs/_build/html/index.html - -servedocs: docs - watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . - -release: clean - python setup.py sdist upload - python setup.py bdist_wheel upload - -dist: clean - python setup.py sdist - python setup.py bdist_wheel - ls -l dist - -install: clean - python setup.py install - -AUTHOR='Jérôme Lafréchoux ' - -extract_messages: - python setup.py extract_messages - cat marshmallow_messages.pot >> messages.pot - # There is currently no way to pass this as an option to pybabel - # https://github.com/python-babel/babel/issues/82 - sed -i s/"FIRST AUTHOR "/$(AUTHOR)/ messages.pot - -update_flask_example_messages: - pybabel update -i messages.pot -l fr -d examples/flask/translations/ - -compile_flask_example_messages: - pybabel compile -d examples/flask/translations/ diff --git a/README.rst b/README.rst index 24b77f87..830cbbea 100644 --- a/README.rst +++ b/README.rst @@ -2,31 +2,29 @@ μMongo: sync/async ODM ====================== -.. image:: https://img.shields.io/pypi/v/umongo.svg - :target: https://pypi.python.org/pypi/umongo - :alt: Latest version +|pypi| |build-status| |pre-commit| |docs| |coverage| -.. image:: https://img.shields.io/pypi/pyversions/umongo.svg +.. |pypi| image:: https://badgen.net/pypi/v/umongo :target: https://pypi.org/project/umongo/ - :alt: Python versions + :alt: Latest version -.. image:: https://img.shields.io/badge/marshmallow-3-blue.svg - :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html - :alt: marshmallow 3 only +.. |build-status| image:: https://github.com/Scille/umongo/actions/workflows/build-release.yml/badge.svg + :target: https://github.com/Scille/umongo/actions/workflows/build-release.yml + :alt: Build status -.. image:: https://img.shields.io/pypi/l/umongo.svg - :target: https://umongo.readthedocs.io/en/latest/license.html - :alt: License +.. |pre-commit| image:: https://results.pre-commit.ci/badge/github/Scille/umongo/main.svg + :target: https://results.pre-commit.ci/latest/github/Scille/umongo/main + :alt: pre-commit.ci status -.. image:: https://dev.azure.com/lafrech/umongo/_apis/build/status/Scille.umongo?branchName=master - :target: https://dev.azure.com/lafrech/umongo/_build/latest?definitionId=1&branchName=master - :alt: Build status +.. |docs| image:: https://readthedocs.org/projects/umongo/badge/ + :target: https://umongo.readthedocs.io/ + :alt: Documentation -.. image:: https://readthedocs.org/projects/umongo/badge/ - :target: http://umongo.readthedocs.io/ - :alt: Documentation +.. |coverage| image:: https://codecov.io/github/Scille/umongo/graph/badge.svg + :target: https://codecov.io/github/Scille/umongo + :alt: Coverage -μMongo is a Python MongoDB ODM. It inception comes from two needs: +μMongo is a Python MongoDB ODM. Its inception comes from two needs: the lack of async ODM and the difficulty to do document (un)serialization with existing ODMs. @@ -41,14 +39,6 @@ From this point, μMongo made a few design choices: - Free software: MIT license - Test with 90%+ coverage ;-) -.. _PyMongo: https://api.mongodb.org/python/current/ -.. _TxMongo: https://txmongo.readthedocs.org/en/latest/ -.. _motor_asyncio: https://motor.readthedocs.org/en/stable/ -.. _mongomock: https://github.com/vmalloc/mongomock -.. _Marshmallow: http://marshmallow.readthedocs.org - -µMongo requires MongoDB 4.2+ and Python 3.7+. - Quick example .. code-block:: python @@ -61,28 +51,32 @@ Quick example db = MongoClient().test instance = PyMongoInstance(db) + @instance.register class User(Document): email = fields.EmailField(required=True, unique=True) - birthday = fields.DateTimeField(validate=validate.Range(min=dt.datetime(1900, 1, 1))) + birthday = fields.DateTimeField( + validate=validate.Range(min=dt.datetime(1900, 1, 1)) + ) friends = fields.ListField(fields.ReferenceField("User")) class Meta: collection_name = "user" + # Make sure that unique indexes are created User.ensure_indexes() - goku = User(email='goku@sayen.com', birthday=dt.datetime(1984, 11, 20)) + goku = User(email="goku@sayen.com", birthday=dt.datetime(1984, 11, 20)) goku.commit() - vegeta = User(email='vegeta@over9000.com', friends=[goku]) + vegeta = User(email="vegeta@over9000.com", friends=[goku]) vegeta.commit() vegeta.friends # ])> vegeta.dump() # {id': '570ddb311d41c89cabceeddc', 'email': 'vegeta@over9000.com', friends': ['570ddb2a1d41c89cabceeddb']} - User.find_one({"email": 'goku@sayen.com'}) + User.find_one({"email": "goku@sayen.com"}) # , # 'email': 'goku@sayen.com', 'birthday': datetime.datetime(1984, 11, 20, 0, 0)})> @@ -96,3 +90,21 @@ Or to get it along with the MongoDB driver you're planing to use:: $ pip install umongo[motor] $ pip install umongo[txmongo] $ pip install umongo[mongomock] + +Support umongo +============== + +If you'd like to support the future of the project, please consider +contributing to Marshmallow_'s Open Collective: + +.. image:: https://opencollective.com/marshmallow/donate/button.png + :target: https://opencollective.com/marshmallow + :width: 200 + :alt: Donate to our collective + + +.. _PyMongo: https://api.mongodb.org/python/current/ +.. _TxMongo: https://txmongo.readthedocs.org/en/latest/ +.. _motor_asyncio: https://motor.readthedocs.org/en/stable/ +.. _mongomock: https://github.com/vmalloc/mongomock +.. _Marshmallow: http://marshmallow.readthedocs.org diff --git a/RELEASING.rst b/RELEASING.rst index c0b09f5b..c7f17982 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -2,27 +2,37 @@ Releasing μMongo ================ -Prerequisites -------------- - -- Install bumpversion_. The easiest way is to create and activate a virtualenv, - and then run ``pip install -r requirements_dev.txt``. - Steps ----- -#. Add an entry to ``HISTORY.rst``, or update the ``Unreleased`` entry, with the +#. Add an entry to ``CHANGELOG.rst``, or update the ``Unreleased`` entry, with the new version and the date of release. Include any bug fixes, features, or backwards incompatibilities included in this release. -#. Commit the changes to ``HISTORY.rst``. -#. Run bumpversion_ to update the version string in ``umongo/__init__.py`` and - ``setup.py``. - * You can combine this step and the previous one by using the ``--allow-dirty`` - flag when running bumpversion_ to make a single release commit. +#. Commit the changes to ``CHANGELOG.rst``. + + $ git add CHANGELOG.rst + $ git commit -m "Update CHANGELOG.rst" + +#. Update ``messages.pot`` file using ``extract`` command and manual edition. + + $ pybabel extract -o messages.pot . --project=umongo --copyright-holder="Scille SAS and contributors" + +#. Update .po and .mo files. + + $ pybabel update -d examples/flask/translations/ -i messages.pot + $ pybabel compile -d examples/flask/translations/ + +#. Update version number in ``pyproject.toml`` then commit. + + $ git add pyproject.toml + $ git commit -m "Bump version" + +#. Create the new version tag (replace with actual version). + + $ git tag x.y.z -#. Run ``git push`` to push the release commits to github. -#. Once the CI tests pass, run ``git push --tags`` to push the tag to github and - trigger the release to pypi. +#. Push the release commits and tag. -.. _bumpversion: https://pypi.org/project/bumpversion/ + $ git push + $ git push origin x.y.z diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 223868ea..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,58 +0,0 @@ -trigger: - branches: - include: [master, test-me-*] - tags: - include: ['*'] - -resources: - repositories: - - repository: sloria - type: github - endpoint: github - name: sloria/azure-pipeline-templates - ref: refs/heads/sloria - -stages: - - stage: lint - jobs: - - template: job--python-tox.yml@sloria - parameters: - toxenvs: [lint] - coverage: false - - stage: test_mongo_4_2 - jobs: - - template: job--python-tox.yml@sloria - parameters: - toxenvs: - - py39-pymongo - - py39-motor - - py39-txmongo - coverage: true - pre_test: - - script: | - sudo rm /etc/apt/sources.list.d/mongodb-org-4.4.list - wget -qO - https://www.mongodb.org/static/pgp/server-4.2.asc | sudo apt-key add - - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.2.list - sudo apt-get remove '^mongodb-org.*' - sudo apt-get update - sudo apt-get install -y mongodb-org - - script: mongod --version - - script: sudo systemctl start mongod - - stage: test_mongo_4_4 - jobs: - - template: job--python-tox.yml@sloria - parameters: - toxenvs: - - py37-pymongo - - py37-motor - - py37-txmongo - - py39-pymongo - - py39-motor - - py39-txmongo - coverage: true - pre_test: - - script: mongod --version - - script: sudo systemctl start mongod - - stage: release - jobs: - - template: job--pypi-release.yml@sloria diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 00000000..565b0521 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py old mode 100755 new mode 100644 index 467f493f..dec005f0 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,285 +1,105 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# umongo documentation build configuration file, created by -# sphinx-quickstart on Tue Jul 9 22:26:36 2013. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - +import importlib.metadata import sys -import os - -# If extensions (or modules to document with autodoc) are in another -# directory, add these directories to sys.path here. If the directory is -# relative to the documentation root, use os.path.abspath to make it -# absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +from pathlib import Path # Get the project root dir, which is the parent dir of this -cwd = os.getcwd() -project_root = os.path.dirname(cwd) +project_root = Path.cwd().parent # Insert the project root dir as the first element in the PYTHONPATH. # This lets us ensure that the source package is imported, and that its # version is used. sys.path.insert(0, project_root) -import umongo # noqa E402 # -- General configuration --------------------------------------------- -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", ] intersphinx_mapping = { - 'pymongo': ('https://pymongo.readthedocs.io/en/latest/', None), - 'marshmallow': ('https://marshmallow.readthedocs.io/en/latest/', None), - 'asyncio': ('https://asyncio.readthedocs.io/en/latest/', None), + "pymongo": ("https://pymongo.readthedocs.io/en/latest/", None), + "marshmallow": ("https://marshmallow.readthedocs.io/en/latest/", None), } # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'uMongo' -copyright = u'2016-2020, Scille SAS and contributors' +project = "uMongo" +copyright = "2016-2020, Scille SAS and contributors" # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout # the built documents. -# -# The short X.Y version. -version = umongo.__version__ -# The full version, including alpha/beta/rc tags. -release = umongo.__version__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None -# There are two options for replacing |today|: either, you set today to -# some non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +version = release = importlib.metadata.version("umongo") # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False +exclude_patterns = ["_build"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built -# documents. -#keep_warnings = False - +pygments_style = "sphinx" # -- Options for HTML output ------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a -# theme further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as -# html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the -# top of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon -# of the docs. This file should be a Windows icon file (.ico) being -# 16x16 or 32x32 pixels large. -#html_favicon = None +html_theme = "default" # Add any paths that contain custom static files (such as style sheets) # here, relative to this directory. They are copied after the builtin # static files, so a file named "default.css" will overwrite the builtin # "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names -# to template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. -# Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. -# Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages -# will contain a tag referring to it. The value of this option -# must be the base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +html_static_path = ["_static"] # Output file base name for HTML help builder. -htmlhelp_basename = 'umongodoc' +htmlhelp_basename = "umongodoc" # -- Options for LaTeX output ------------------------------------------ -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - #'preamble': '', -} +latex_elements = {} # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ - ('index', 'umongo.tex', - u'uMongo Documentation', - u'Scille SAS', 'manual'), + ("index", "umongo.tex", "uMongo Documentation", "Scille SAS", "manual"), ] -# The name of an image file (relative to this directory) to place at -# the top of the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings -# are parts, not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - # -- Options for manual page output ------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'umongo', - u'uMongo Documentation', - [u'Scille SAS'], 1) + ("index", "umongo", "uMongo Documentation", ["Scille SAS"], 1), ] -# If true, show URL addresses after external links. -#man_show_urls = False - - # -- Options for Texinfo output ---------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'umongo', - u'uMongo Documentation', - u'Scille SAS', - 'umongo', - 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "umongo", + "uMongo Documentation", + "Scille SAS", + "umongo", + "One line description of project.", + "Miscellaneous", + ), ] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst index 0522129c..2808a291 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ Contents: apireference contributing authors - history + changelog Indices and tables ------------------ diff --git a/docs/migration.rst b/docs/migration.rst index d43b09b1..ff6307ff 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -42,12 +42,14 @@ For instance, given following umongo 3 application code # Register embedded documents [...] + @instance.register class Doc(Document): name = fields.StrField() # Embed documents embedded = fields.EmbeddedField([...]) + instance.set_db(pymongo.MongoClient()) # This may raise an exception if Doc contains embedded documents @@ -65,12 +67,14 @@ the migration can be performed by calling migrate_2_to_3. # Register embedded documents [...] + @instance.register class Doc(Document): name = fields.StrField() # Embed documents embedded = fields.EmbeddedField([...]) + instance.set_db(pymongo.MongoClient()) instance.migrate_2_to_3() diff --git a/docs/userguide.rst b/docs/userguide.rst index 76ef85c7..4ecfabf1 100644 --- a/docs/userguide.rst +++ b/docs/userguide.rst @@ -35,8 +35,7 @@ Python dict example >>> {"str_field": "hello world", "int_field": 42, "date_field": datetime(2015, 1, 1)} To be integrated into μMongo, those data need to be deserialized and to leave -μMongo they need to be serialized (under the hood μMongo uses -`marshmallow `_ schema). +μMongo they need to be serialized (under the hood μMongo uses `marshmallow`_ schemas). The deserialization operation is done automatically when instantiating a :class:`umongo.Document`. The serialization is done when calling @@ -489,8 +488,7 @@ for a working example of i18n with `flask-babel `_ -for all its data validation work. +Under the hood, μMongo heavily uses `marshmallow`_ for all its data validation work. However an ODM has some special needs (i.g. handling ``required`` fields through MongoDB's unique indexes) that force to extend marshmallow base types. @@ -618,16 +616,16 @@ using pure marshmallow fields generated with the So when you use ``as_marshmallow_field``, the resulting marshmallow field's ``missing``&``default`` will be by default both infered from the umongo's ``default`` field. You can overwrite this behavior by using - ``marshmallow_missing``/``marshmallow_default`` attributes: + ``marshmallow_load_default``/``marshmallow_dump_default`` attributes: .. code-block:: python @instance.register class Employee(Document): name = fields.StrField(default='John Doe') - birthday = fields.DateTimeField(marshmallow_missing=dt.datetime(2000, 1, 1)) + birthday = fields.DateTimeField(marshmallow_load_default=dt.datetime(2000, 1, 1)) # You can use `missing` singleton to overwrite `default` field inference - skill = fields.StrField(default='Dummy', marshmallow_default=missing) + skill = fields.StrField(default='Dummy', marshmallow_dump_default=missing) ret = Employee.schema.as_marshmallow_schema()().load({}) assert ret == {'name': 'John Doe', 'birthday': datetime(2000, 1, 1, 0, 0, tzinfo=tzutc()), 'skill': 'Dummy'} @@ -725,3 +723,6 @@ wrapped by :class:`asyncio.coroutine` and called with ``yield from``. .. warning:: When converting to marshmallow with `as_marshmallow_schema` and `as_marshmallow_fields`, `io_validate` attribute will not be preserved. + + +.. _marshmallow: diff --git a/examples/flask/app.py b/examples/flask/app.py index 4686d0b6..050c5fe7 100644 --- a/examples/flask/app.py +++ b/examples/flask/app.py @@ -1,13 +1,13 @@ import datetime as dt -from flask import Flask, abort, jsonify, request -from flask_babel import Babel, gettext from bson import ObjectId from pymongo import MongoClient -from umongo import Document, fields, ValidationError, RemoveMissingSchema, set_gettext -from umongo.frameworks import PyMongoInstance +from flask import Flask, abort, jsonify, request +from flask_babel import Babel, gettext +from umongo import Document, RemoveMissingSchema, ValidationError, fields, set_gettext +from umongo.frameworks import PyMongoInstance app = Flask(__name__) db = MongoClient().demo_umongo @@ -18,8 +18,8 @@ # available languages LANGUAGES = { - 'en': 'English', - 'fr': 'Français' + "en": "English", + "fr": "Français", } @@ -30,7 +30,6 @@ def get_locale(): @instance.register class User(Document): - # We specify `RemoveMissingSchema` as a base marshmallow schema so that # auto-generated marshmallow schemas skip missing fields instead of returning None MA_BASE_SCHEMA_CLS = RemoveMissingSchema @@ -50,33 +49,54 @@ def populate_db(): User.ensure_indexes() for data in [ { - 'nick': 'mze', 'lastname': 'Mao', 'firstname': 'Zedong', - 'birthday': dt.datetime(1893, 12, 26), 'password': 'Serve the people' + "nick": "mze", + "lastname": "Mao", + "firstname": "Zedong", + "birthday": dt.datetime(1893, 12, 26), + "password": "Serve the people", }, { - 'nick': 'lsh', 'lastname': 'Liu', 'firstname': 'Shaoqi', - 'birthday': dt.datetime(1898, 11, 24), 'password': 'Dare to think, dare to act' + "nick": "lsh", + "lastname": "Liu", + "firstname": "Shaoqi", + "birthday": dt.datetime(1898, 11, 24), + "password": "Dare to think, dare to act", }, { - 'nick': 'lxia', 'lastname': 'Li', 'firstname': 'Xiannian', - 'birthday': dt.datetime(1909, 6, 23), 'password': 'To rebel is justified' + "nick": "lxia", + "lastname": "Li", + "firstname": "Xiannian", + "birthday": dt.datetime(1909, 6, 23), + "password": "To rebel is justified", }, { - 'nick': 'ysh', 'lastname': 'Yang', 'firstname': 'Shangkun', - 'birthday': dt.datetime(1907, 7, 5), 'password': 'Smash the gang of four' + "nick": "ysh", + "lastname": "Yang", + "firstname": "Shangkun", + "birthday": dt.datetime(1907, 7, 5), + "password": "Smash the gang of four", }, { - 'nick': 'jze', 'lastname': 'Jiang', 'firstname': 'Zemin', - 'birthday': dt.datetime(1926, 8, 17), 'password': 'Seek truth from facts' + "nick": "jze", + "lastname": "Jiang", + "firstname": "Zemin", + "birthday": dt.datetime(1926, 8, 17), + "password": "Seek truth from facts", }, { - 'nick': 'huji', 'lastname': 'Hu', 'firstname': 'Jintao', - 'birthday': dt.datetime(1942, 12, 21), 'password': 'It is good to have just 1 child' + "nick": "huji", + "lastname": "Hu", + "firstname": "Jintao", + "birthday": dt.datetime(1942, 12, 21), + "password": "It is good to have just 1 child", }, { - 'nick': 'xiji', 'lastname': 'Xi', 'firstname': 'Jinping', - 'birthday': dt.datetime(1953, 6, 15), 'password': 'Achieve the 4 modernisations' - } + "nick": "xiji", + "lastname": "Xi", + "firstname": "Jinping", + "birthday": dt.datetime(1953, 6, 15), + "password": "Achieve the 4 modernisations", + }, ]: User(**data).commit() @@ -84,7 +104,7 @@ def populate_db(): # Define a custom marshmallow schema to ignore read-only fields class UserUpdateSchema(User.schema.as_marshmallow_schema()): class Meta: - dump_only = ('nick', 'password',) + dump_only = ("nick", "password") user_update_schema = UserUpdateSchema() @@ -93,7 +113,7 @@ class Meta: # Define a custom marshmallow schema from User document to exclude password field class UserNoPassSchema(User.schema.as_marshmallow_schema()): class Meta: - exclude = ('password',) + exclude = ("password",) user_no_pass_schema = UserNoPassSchema() @@ -106,14 +126,14 @@ def dump_user_no_pass(u): # Define a custom marshmallow schema from User document to expose only password field class ChangePasswordSchema(User.schema.as_marshmallow_schema()): class Meta: - fields = ('password',) - required = ('password',) + fields = ("password",) + required = ("password",) change_password_schema = ChangePasswordSchema() -@app.route('/', methods=['GET']) +@app.route("/", methods=["GET"]) def root(): return """

Umongo flask example


@@ -136,10 +156,10 @@ def _to_objid(data): def _nick_or_id_lookup(nick_or_id): - return {'$or': [{'nick': nick_or_id}, {'_id': _to_objid(nick_or_id)}]} + return {"$or": [{"nick": nick_or_id}, {"_id": _to_objid(nick_or_id)}]} -@app.route('/users/', methods=['GET']) +@app.route("/users/", methods=["GET"]) def get_user(nick_or_id): user = User.find_one(_nick_or_id_lookup(nick_or_id)) if not user: @@ -147,11 +167,11 @@ def get_user(nick_or_id): return jsonify(dump_user_no_pass(user)) -@app.route('/users/', methods=['PATCH']) +@app.route("/users/", methods=["PATCH"]) def update_user(nick_or_id): payload = request.get_json() if payload is None: - abort(400, 'Request body must be json with Content-type: application/json') + abort(400, "Request body must be json with Content-type: application/json") user = User.find_one(_nick_or_id_lookup(nick_or_id)) if not user: abort(404) @@ -166,7 +186,7 @@ def update_user(nick_or_id): return jsonify(dump_user_no_pass(user)) -@app.route('/users/', methods=['DELETE']) +@app.route("/users/", methods=["DELETE"]) def delete_user(nick_or_id): user = User.find_one(_nick_or_id_lookup(nick_or_id)) if not user: @@ -177,20 +197,20 @@ def delete_user(nick_or_id): resp = jsonify(message=ve.args[0]) resp.status_code = 400 return resp - return 'Ok' + return "Ok" -@app.route('/users//password', methods=['PUT']) +@app.route("/users//password", methods=["PUT"]) def change_user_password(nick_or_id): payload = request.get_json() if payload is None: - abort(400, 'Request body must be json with Content-type: application/json') + abort(400, "Request body must be json with Content-type: application/json") user = User.find_one(_nick_or_id_lookup(nick_or_id)) if not user: abort(404) try: data = change_password_schema.load(payload) - user.password = data['password'] + user.password = data["password"] user.commit() except ValidationError as ve: resp = jsonify(message=ve.args[0]) @@ -199,23 +219,25 @@ def change_user_password(nick_or_id): return jsonify(dump_user_no_pass(user)) -@app.route('/users', methods=['GET']) +@app.route("/users", methods=["GET"]) def list_users(): - page = int(request.args.get('page', 1)) + page = int(request.args.get("page", 1)) users = User.find().limit(10).skip((page - 1) * 10) - return jsonify({ - '_total': users.count(), - '_page': page, - '_per_page': 10, - '_items': [dump_user_no_pass(u) for u in users] - }) + return jsonify( + { + "_total": users.count(), + "_page": page, + "_per_page": 10, + "_items": [dump_user_no_pass(u) for u in users], + }, + ) -@app.route('/users', methods=['POST']) +@app.route("/users", methods=["POST"]) def create_user(): payload = request.get_json() if payload is None: - abort(400, 'Request body must be json with Content-type: application/json') + abort(400, "Request body must be json with Content-type: application/json") try: user = User(**payload) user.commit() @@ -226,6 +248,6 @@ def create_user(): return jsonify(dump_user_no_pass(user)) -if __name__ == '__main__': +if __name__ == "__main__": populate_db() app.run(debug=True) diff --git a/examples/flask/testbed.py b/examples/flask/testbed.py index e18dd3a5..706f8e5a 100644 --- a/examples/flask/testbed.py +++ b/examples/flask/testbed.py @@ -4,123 +4,130 @@ class Tester: - def __init__(self, test_name): self.name = test_name def __enter__(self): - print('%s...' % self.name, flush=True, end='') + print(f"{self.name}...", flush=True, end="") return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type: - print(' Error !') + print(" Error !") else: - print(' OK') + print(" OK") def test_list(total): - r = requests.get('http://localhost:5000/users') + r = requests.get("http://localhost:5000/users") assert r.status_code == 200, r.status_code data = r.json() - assert data['_total'] == total, 'expected %s, got %s' % (total, data['_total']) - assert len(data['_items']) == total, 'expected %s, got %s' % (total, len(data['_items'])) + assert data["_total"] == total, "expected {}, got {}".format(total, data["_total"]) + assert len(data["_items"]) == total, "expected {}, got {}".format( + total, + len(data["_items"]), + ) return data -with Tester('List all'): +with Tester("List all"): data = test_list(7) -with Tester('Get one by id'): - user = data['_items'][0] - r = requests.get('http://localhost:5000/users/%s' % user['id']) +with Tester("Get one by id"): + user = data["_items"][0] + r = requests.get("http://localhost:5000/users/{}".format(user["id"])) assert r.status_code == 200, r.status_code data = r.json() - assert user == data, 'user: %s, data: %s' % (user, data) + assert user == data, f"user: {user}, data: {data}" -with Tester('Get one by nick'): - r = requests.get('http://localhost:5000/users/%s' % user['nick']) +with Tester("Get one by nick"): + r = requests.get("http://localhost:5000/users/{}".format(user["nick"])) assert r.status_code == 200, r.status_code - assert data == r.json(), 'data: %s, nick_data: %s' % (data, r.json()) + assert data == r.json(), f"data: {data}, nick_data: {r.json()}" -with Tester('404 on one'): - r = requests.get('http://localhost:5000/users/572c59bf13abf21bf84890a0') +with Tester("404 on one"): + r = requests.get("http://localhost:5000/users/572c59bf13abf21bf84890a0") assert r.status_code == 404, r.status_code -with Tester('Create one'): +with Tester("Create one"): payload = { - 'nick': 'n00b', - 'birthday': '2016-05-18T11:40:32+00:00', - 'password': '123456' + "nick": "n00b", + "birthday": "2016-05-18T11:40:32+00:00", + "password": "123456", } - r = requests.post('http://localhost:5000/users', json=payload) + r = requests.post("http://localhost:5000/users", json=payload) assert r.status_code == 200, r.status_code data = r.json() - new_user_id = data.pop('id') + new_user_id = data.pop("id") expected = { - 'nick': 'n00b', - 'birthday': '2016-05-18T11:40:32+00:00', + "nick": "n00b", + "birthday": "2016-05-18T11:40:32+00:00", } - assert data == expected, 'data: %s, expected: %s' % (data, expected) + assert data == expected, f"data: {data}, expected: {expected}" test_list(8) -with Tester('Update'): +with Tester("Update"): payload = { - 'birthday': '2019-05-18T11:40:32+00:00', + "birthday": "2019-05-18T11:40:32+00:00", } - r = requests.patch('http://localhost:5000/users/%s' % new_user_id, - json=payload) + r = requests.patch(f"http://localhost:5000/users/{new_user_id}", json=payload) assert r.status_code == 200, r.status_code data = r.json() - del data['id'] + del data["id"] expected = { - 'nick': 'n00b', - 'birthday': '2019-05-18T11:40:32+00:00', + "nick": "n00b", + "birthday": "2019-05-18T11:40:32+00:00", } - assert data == expected, 'data: %s, expected: %s' % (data, expected) + assert data == expected, f"data: {data}, expected: {expected}" test_list(8) -with Tester('Change password'): - r = requests.put('http://localhost:5000/users/%s/password' % new_user_id, - json={'password': 'abcdef'}) +with Tester("Change password"): + r = requests.put( + f"http://localhost:5000/users/{new_user_id}/password", + json={"password": "abcdef"}, + ) assert r.status_code == 200, r.status_code data = r.json() - assert new_user_id == data.pop('id') - assert data == expected, 'data: %s, expected: %s' % (data, expected) - -with Tester('Bad change password'): - r = requests.put('http://localhost:5000/users/%s/password' % new_user_id, - json={'password': 'abcdef', 'dummy': 42}) + assert new_user_id == data.pop("id") + assert data == expected, f"data: {data}, expected: {expected}" + +with Tester("Bad change password"): + r = requests.put( + f"http://localhost:5000/users/{new_user_id}/password", + json={"password": "abcdef", "dummy": 42}, + ) assert r.status_code == 400, r.status_code data = r.json() - expected = {'message': {'dummy': ['Unknown field.']}} - assert data == expected, 'data: %s, expected: %s' % (data, expected) - -with Tester('404 on change password'): - r = requests.put('http://localhost:5000/users/572c59bf13abf21bf84890a0/password', - json={'password': 'abcdef'}) + expected = {"message": {"dummy": ["Unknown field."]}} + assert data == expected, f"data: {data}, expected: {expected}" + +with Tester("404 on change password"): + r = requests.put( + "http://localhost:5000/users/572c59bf13abf21bf84890a0/password", + json={"password": "abcdef"}, + ) assert r.status_code == 404, r.status_code -with Tester('Delete one'): - r = requests.delete('http://localhost:5000/users/%s' % new_user_id) +with Tester("Delete one"): + r = requests.delete(f"http://localhost:5000/users/{new_user_id}") assert r.status_code == 200, r.status_code test_list(7) -with Tester('404 on delete one'): - r = requests.delete('http://localhost:5000/users/572c59bf13abf21bf84890a0') +with Tester("404 on delete one"): + r = requests.delete("http://localhost:5000/users/572c59bf13abf21bf84890a0") assert r.status_code == 404, r.status_code -with Tester('Create one missing field'): - r = requests.post('http://localhost:5000/users', json={}) +with Tester("Create one missing field"): + r = requests.post("http://localhost:5000/users", json={}) assert r.status_code == 400, r.status_code data = r.json() - expected = {'message': {'nick': ['Missing data for required field.']}} - assert data == expected, 'data: %s, expected: %s' % (data, expected) + expected = {"message": {"nick": ["Missing data for required field."]}} + assert data == expected, f"data: {data}, expected: {expected}" -with Tester('Create one i18n'): - headers = {'Accept-Language': 'fr, en-gb;q=0.8, en;q=0.7'} - r = requests.post('http://localhost:5000/users', headers=headers, json={}) +with Tester("Create one i18n"): + headers = {"Accept-Language": "fr, en-gb;q=0.8, en;q=0.7"} + r = requests.post("http://localhost:5000/users", headers=headers, json={}) assert r.status_code == 400, r.status_code data = r.json() - expected = {'message': {'nick': ['Valeur manquante pour un champ obligatoire.']}} - assert data == expected, 'data: %s, expected: %s' % (data, expected) + expected = {"message": {"nick": ["Valeur manquante pour un champ obligatoire."]}} + assert data == expected, f"data: {data}, expected: {expected}" diff --git a/examples/flask/translations/fr/LC_MESSAGES/messages.mo b/examples/flask/translations/fr/LC_MESSAGES/messages.mo index c8613946..b91fad5c 100644 Binary files a/examples/flask/translations/fr/LC_MESSAGES/messages.mo and b/examples/flask/translations/fr/LC_MESSAGES/messages.mo differ diff --git a/examples/flask/translations/fr/LC_MESSAGES/messages.po b/examples/flask/translations/fr/LC_MESSAGES/messages.po index af48108a..f502c6d3 100644 --- a/examples/flask/translations/fr/LC_MESSAGES/messages.po +++ b/examples/flask/translations/fr/LC_MESSAGES/messages.po @@ -7,169 +7,180 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: jerome@jolimont.fr\n" -"POT-Creation-Date: 2021-01-04 15:53+0100\n" +"POT-Creation-Date: 2025-10-07 00:07+0200\n" "PO-Revision-Date: 2016-04-19 12:09+0200\n" "Last-Translator: Jérôme Lafréchoux \n" "Language: fr\n" "Language-Team: fr \n" -"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.7.0\n" +"Generated-By: Babel 2.17.0\n" -#: umongo/abstract.py:103 +#: src/umongo/abstract.py:95 msgid "Field value must be unique." msgstr "La valeur de ce champ doit être unique." -#: umongo/abstract.py:104 +#: src/umongo/abstract.py:96 +#, python-brace-format msgid "Values of fields {fields} must be unique together." msgstr "L'ensemble des valeurs des champs {fields} doit être unique." -#: umongo/data_objects.py:155 +#: src/umongo/data_objects.py:157 +#, python-brace-format msgid "Reference not found for document {document}." msgstr "Référence non trouvée pour document {document}." -#: umongo/data_proxy.py:65 -msgid "{cls}: unknown \"{key}\" field found in DB." -msgstr "{cls}: Champ \"{key}\" inconnu trouvé dans la base de données." +#: src/umongo/data_proxy.py:65 +#, python-brace-format, python-format +msgid "%s: unknown \"%s\" field found in DB." +msgstr "%s: champ \"%s\" inconnu trouvé dans la base de données." # Marshmallow fields # -#: umongo/data_proxy.py:170 +#: src/umongo/data_proxy.py:171 msgid "Missing data for required field." msgstr "Valeur manquante pour un champ obligatoire." -#: umongo/fields.py:347 +#: src/umongo/fields.py:350 +#, python-brace-format msgid "DBRef must be on collection `{collection}`." msgstr "DBRef doit être sur la collection `{collection}`." -#: umongo/fields.py:352 umongo/fields.py:363 +#: src/umongo/fields.py:358 src/umongo/fields.py:373 +#, python-brace-format msgid "`{document}` reference expected." msgstr "Référence sur `{document}` attendue." -#: umongo/fields.py:360 umongo/fields.py:404 +#: src/umongo/fields.py:368 src/umongo/fields.py:416 msgid "Cannot reference a document that has not been created yet." msgstr "Impossible de référencer un document qui n'a pas été encore créé." -#: umongo/fields.py:386 umongo/fields.py:483 +#: src/umongo/fields.py:398 src/umongo/fields.py:503 +#, python-brace-format msgid "Unknown document `{document}`." msgstr "Document unconnu `{document}`." -#: umongo/fields.py:408 umongo/marshmallow_bonus.py:65 +#: src/umongo/fields.py:422 src/umongo/marshmallow_bonus.py:61 msgid "Generic reference must have `id` and `cls` fields." msgstr "Une référence générique doit avoir des champs `id` et `cls`." -#: umongo/fields.py:412 umongo/marshmallow_bonus.py:69 +#: src/umongo/fields.py:427 src/umongo/marshmallow_bonus.py:66 msgid "Invalid `id` field." msgstr "Champ `id` invalide." -#: umongo/fields.py:415 umongo/marshmallow_bonus.py:63 +#: src/umongo/fields.py:430 src/umongo/marshmallow_bonus.py:58 msgid "Invalid value for generic reference field." msgstr "Valeur de référence générique invalide." -#: umongo/marshmallow_bonus.py:29 +#: src/umongo/marshmallow_bonus.py:28 msgid "Invalid ObjectId." msgstr "ObjectId invalide." -msgid "Invalid input type." -msgstr "Type en entrée invalide." +#: tests/test_i18n.py:26 +msgid "hello" +msgstr "" + +#~ msgid "Invalid input type." +#~ msgstr "Type en entrée invalide." -msgid "Field may not be null." -msgstr "Ce champ ne doit pas être vide." +#~ msgid "Field may not be null." +#~ msgstr "Ce champ ne doit pas être vide." -msgid "Invalid value." -msgstr "Valeur invalide." +#~ msgid "Invalid value." +#~ msgstr "Valeur invalide." -msgid "Invalid type." -msgstr "Type invalide." +#~ msgid "Invalid type." +#~ msgstr "Type invalide." -msgid "Not a valid list." -msgstr "Pas une liste valide." +#~ msgid "Not a valid list." +#~ msgstr "Pas une liste valide." -msgid "Not a valid string." -msgstr "Pas une chaîne de charactères valide." +#~ msgid "Not a valid string." +#~ msgstr "Pas une chaîne de charactères valide." -msgid "Not a valid number." -msgstr "Pas un nombre valide." +#~ msgid "Not a valid number." +#~ msgstr "Pas un nombre valide." -msgid "Not a valid integer." -msgstr "Pas un nombre entier valide." +#~ msgid "Not a valid integer." +#~ msgstr "Pas un nombre entier valide." -msgid "Special numeric values are not permitted." -msgstr "Nombre spéciaux non autorisés." +#~ msgid "Special numeric values are not permitted." +#~ msgstr "Nombre spéciaux non autorisés." -msgid "Not a valid boolean." -msgstr "Pas un booléen valide." +#~ msgid "Not a valid boolean." +#~ msgstr "Pas un booléen valide." -msgid "Cannot format string with given data." -msgstr "Impossible de formater la chaine de caractère avec ces données" +#~ msgid "Cannot format string with given data." +#~ msgstr "Impossible de formater la chaine de caractère avec ces données" -msgid "Not a valid datetime." -msgstr "Pas une datetime valide." +#~ msgid "Not a valid datetime." +#~ msgstr "Pas une datetime valide." -msgid "\"{input}\" cannot be formatted as a datetime." -msgstr "\"{input}\" ne peut pas être formaté comme une datetime." +#~ msgid "\"{input}\" cannot be formatted as a datetime." +#~ msgstr "\"{input}\" ne peut pas être formaté comme une datetime." -msgid "Not a valid time." -msgstr "Pas un temps valide." +#~ msgid "Not a valid time." +#~ msgstr "Pas un temps valide." -msgid "\"{input}\" cannot be formatted as a time." -msgstr "\"{input}\" ne peut pas être formatée comme un temps." +#~ msgid "\"{input}\" cannot be formatted as a time." +#~ msgstr "\"{input}\" ne peut pas être formatée comme un temps." -msgid "Not a valid date." -msgstr "Pas une date valide." +#~ msgid "Not a valid date." +#~ msgstr "Pas une date valide." -msgid "\"{input}\" cannot be formatted as a date." -msgstr "\"{input}\" ne peut pas être formatée comme une date." +#~ msgid "\"{input}\" cannot be formatted as a date." +#~ msgstr "\"{input}\" ne peut pas être formatée comme une date." -msgid "Not a valid period of time." -msgstr "Pas une période de temps valide." +#~ msgid "Not a valid period of time." +#~ msgstr "Pas une période de temps valide." -msgid "{input!r} cannot be formatted as a timedelta." -msgstr "{input!r} ne peut pas être formatée comme un timedelta." +#~ msgid "{input!r} cannot be formatted as a timedelta." +#~ msgstr "{input!r} ne peut pas être formatée comme un timedelta." -msgid "Not a valid mapping type." -msgstr "Pas un mapping valide." +#~ msgid "Not a valid mapping type." +#~ msgstr "Pas un mapping valide." # Marshmallow validate # -msgid "Not a valid URL." -msgstr "Pas une URL valide." +#~ msgid "Not a valid URL." +#~ msgstr "Pas une URL valide." + +#~ msgid "Not a valid email address." +#~ msgstr "Pas une adresse mail valide." -msgid "Not a valid email address." -msgstr "Pas une adresse mail valide." +#~ msgid "Must be at least {min}." +#~ msgstr "Doit être au moins {min}." -msgid "Must be at least {min}." -msgstr "Doit être au moins {min}." +#~ msgid "Must be at most {max}." +#~ msgstr "Doit être au plus {max}." -msgid "Must be at most {max}." -msgstr "Doit être au plus {max}." +#~ msgid "Must be between {min} and {max}." +#~ msgstr "Doit être entre {min} et {max}." -msgid "Must be between {min} and {max}." -msgstr "Doit être entre {min} et {max}." +#~ msgid "Shorter than minimum length {min}." +#~ msgstr "Plus court que la longueur minimal de {min}." -msgid "Shorter than minimum length {min}." -msgstr "Plus court que la longueur minimal de {min}." +#~ msgid "Longer than maximum length {max}." +#~ msgstr "Plus long que la longueur maximale de {max}." -msgid "Longer than maximum length {max}." -msgstr "Plus long que la longueur maximale de {max}." +#~ msgid "Length must be between {min} and {max}." +#~ msgstr "La longueur doit être entre {min} et {max}." -msgid "Length must be between {min} and {max}." -msgstr "La longueur doit être entre {min} et {max}." +#~ msgid "Length must be {equal}." +#~ msgstr "La longueur doit être de {equal}." -msgid "Length must be {equal}." -msgstr "La longueur doit être de {equal}." +#~ msgid "Must be equal to {other}." +#~ msgstr "Doit être égal à {other}." -msgid "Must be equal to {other}." -msgstr "Doit être égal à {other}." +#~ msgid "String does not match expected pattern." +#~ msgstr "La chaîne de caractère ne doit pas valider l'expression rationnelle." -msgid "String does not match expected pattern." -msgstr "La chaîne de caractère ne doit pas valider l'expression rationnelle." +#~ msgid "Invalid input." +#~ msgstr "Entrée invalide." -msgid "Invalid input." -msgstr "Entrée invalide." +#~ msgid "Not a valid choice." +#~ msgstr "Pas un choix valide." -msgid "Not a valid choice." -msgstr "Pas un choix valide." +#~ msgid "One or more of the choices you made was not acceptable." +#~ msgstr "Un ou plusieurs des choix faits n'est pas acceptable." -msgid "One or more of the choices you made was not acceptable." -msgstr "Un ou plusieurs des choix faits n'est pas acceptable." diff --git a/examples/inheritance/app.py b/examples/inheritance/app.py index 9bc3a050..de43cd6f 100644 --- a/examples/inheritance/app.py +++ b/examples/inheritance/app.py @@ -1,10 +1,9 @@ from bson import ObjectId from pymongo import MongoClient -from umongo import Document, fields, ValidationError, validate +from umongo import Document, ValidationError, fields, validate from umongo.frameworks import PyMongoInstance - db = MongoClient().demo_umongo instance = PyMongoInstance(db) @@ -24,25 +23,24 @@ class Car(Vehicle): @instance.register class MotorBike(Vehicle): - engine_type = fields.StrField(validate=validate.OneOf(['2-stroke', '4-stroke'])) + engine_type = fields.StrField(validate=validate.OneOf(["2-stroke", "4-stroke"])) def populate_db(): Vehicle.collection.drop() Vehicle.ensure_indexes() for data in [ - {'model': 'Chevrolet Impala 1966', 'doors': 5}, - {'model': 'Ford Grand Torino', 'doors': 3}, + {"model": "Chevrolet Impala 1966", "doors": 5}, + {"model": "Ford Grand Torino", "doors": 3}, ]: Car(**data).commit() for data in [ - {'model': 'Honda CB125', 'engine_type': '2-stroke'} + {"model": "Honda CB125", "engine_type": "2-stroke"}, ]: MotorBike(**data).commit() -class Repl(object): - +class Repl: USAGE = """help: print this message new: create a vehicle ls: list vehicles @@ -55,49 +53,49 @@ def get_vehicle(self, *args): id = args[0] vehicle = None try: - vehicle = Vehicle.find_one({'_id': ObjectId(id)}) + vehicle = Vehicle.find_one({"_id": ObjectId(id)}) except Exception as exc: - print('Error: %s' % exc) + print(f"Error: {exc}") return if vehicle: print(vehicle) else: - print('Error: unknown vehicle `%s`' % id) + print(f"Error: unknown vehicle `{id}`") def list_vehicles(self): - print('Found %s vehicles' % Vehicle.find().count()) - print('\n'.join([str(v) for v in Vehicle.find()])) + print(f"Found {Vehicle.find().count()} vehicles") + print("\n".join([str(v) for v in Vehicle.find()])) def new_vehicle(self): - vehicle_type = input('Type ? car/bike ') or 'car' + vehicle_type = input("Type ? car/bike ") or "car" data = { - 'model': input('Model ? ') or 'unknown' + "model": input("Model ? ") or "unknown", } - if vehicle_type == 'car': + if vehicle_type == "car": try: - data['doors'] = int(input('# of doors ? 3/5 ')) + data["doors"] = int(input("# of doors ? 3/5 ")) except ValueError: pass vehicle = Car(**data) else: - strokes = input('Type of stroke-engine ? 2/4 ') + strokes = input("Type of stroke-engine ? 2/4 ") if strokes: - data['engine_type'] = '2-stroke' if strokes == '2' else '4-stroke' + data["engine_type"] = "2-stroke" if strokes == "2" else "4-stroke" vehicle = MotorBike(**data) try: vehicle.commit() except ValidationError as exc: - print('Error: %s' % exc) + print(f"Error: {exc}") else: - print('Created %s' % vehicle) + print(f"Created {vehicle}") def start(self): quit = False print("Welcome to the garage, type `help` if you're lost") while not quit: - cmd = input('> ') + cmd = input("> ") cmd = cmd.strip() - if cmd == 'help': + if cmd == "help": print(self.USAGE) elif cmd.startswith("ls"): self.list_vehicles() @@ -105,12 +103,12 @@ def start(self): self.new_vehicle() elif cmd.startswith("get"): self.get_vehicle(*cmd.split()[1:]) - elif cmd == 'quit': + elif cmd == "quit": quit = True else: - print('Error: Unknow command !') + print("Error: Unknow command !") -if __name__ == '__main__': +if __name__ == "__main__": populate_db() Repl().start() diff --git a/examples/klein/app.py b/examples/klein/app.py index 186f8ac5..41d4ce45 100644 --- a/examples/klein/app.py +++ b/examples/klein/app.py @@ -1,14 +1,15 @@ import datetime as dt import json -from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks, returnValue -from klein import Klein from bson import ObjectId from txmongo import MongoConnection + +from klein import Klein from klein_babel import gettext, locale_from_request +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks, returnValue -from umongo import Document, fields, ValidationError, RemoveMissingSchema, set_gettext +from umongo import Document, RemoveMissingSchema, ValidationError, fields, set_gettext from umongo.frameworks import PyMongoInstance app = Klein() @@ -21,16 +22,14 @@ class MongoJsonEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, (dt.datetime, dt.date)): return obj.isoformat() - elif isinstance(obj, ObjectId): + if isinstance(obj, ObjectId): return str(obj) return json.JSONEncoder.default(self, obj) def jsonify(request, *args, **kwargs): - """ - jsonify with support for MongoDB ObjectId - """ - request.setHeader('Content-Type', 'application/json') + """Jsonify with support for MongoDB ObjectId""" + request.setHeader("Content-Type", "application/json") return json.dumps(dict(*args, **kwargs), cls=MongoJsonEncoder, indent=True) @@ -40,7 +39,6 @@ def get_json(request): @instance.register class User(Document): - # We specify `RemoveMissingSchema` as a base marshmallow schema so that # auto-generated marshmallow schemas skip missing fields instead of returning None MA_BASE_SCHEMA_CLS = RemoveMissingSchema @@ -58,40 +56,54 @@ def populate_db(): yield User.ensure_indexes() for data in [ { - 'nick': 'mze', 'lastname': 'Mao', 'firstname': 'Zedong', - 'birthday': dt.datetime(1893, 12, 26), - 'password': 'Serve the people' + "nick": "mze", + "lastname": "Mao", + "firstname": "Zedong", + "birthday": dt.datetime(1893, 12, 26), + "password": "Serve the people", }, { - 'nick': 'lsh', 'lastname': 'Liu', 'firstname': 'Shaoqi', - 'birthday': dt.datetime(1898, 11, 24), - 'password': 'Dare to think, dare to act' + "nick": "lsh", + "lastname": "Liu", + "firstname": "Shaoqi", + "birthday": dt.datetime(1898, 11, 24), + "password": "Dare to think, dare to act", }, { - 'nick': 'lxia', 'lastname': 'Li', 'firstname': 'Xiannian', - 'birthday': dt.datetime(1909, 6, 23), - 'password': 'To rebel is justified' + "nick": "lxia", + "lastname": "Li", + "firstname": "Xiannian", + "birthday": dt.datetime(1909, 6, 23), + "password": "To rebel is justified", }, { - 'nick': 'ysh', 'lastname': 'Yang', 'firstname': 'Shangkun', - 'birthday': dt.datetime(1907, 7, 5), - 'password': 'Smash the gang of four' + "nick": "ysh", + "lastname": "Yang", + "firstname": "Shangkun", + "birthday": dt.datetime(1907, 7, 5), + "password": "Smash the gang of four", }, { - 'nick': 'jze', 'lastname': 'Jiang', 'firstname': 'Zemin', - 'birthday': dt.datetime(1926, 8, 17), - 'password': 'Seek truth from facts' + "nick": "jze", + "lastname": "Jiang", + "firstname": "Zemin", + "birthday": dt.datetime(1926, 8, 17), + "password": "Seek truth from facts", }, { - 'nick': 'huji', 'lastname': 'Hu', 'firstname': 'Jintao', - 'birthday': dt.datetime(1942, 12, 21), - 'password': 'It is good to have just 1 child' + "nick": "huji", + "lastname": "Hu", + "firstname": "Jintao", + "birthday": dt.datetime(1942, 12, 21), + "password": "It is good to have just 1 child", }, { - 'nick': 'xiji', 'lastname': 'Xi', 'firstname': 'Jinping', - 'birthday': dt.datetime(1953, 6, 15), - 'password': 'Achieve the 4 modernisations' - } + "nick": "xiji", + "lastname": "Xi", + "firstname": "Jinping", + "birthday": dt.datetime(1953, 6, 15), + "password": "Achieve the 4 modernisations", + }, ]: yield User(**data).commit() @@ -99,7 +111,7 @@ def populate_db(): # Define a custom marshmallow schema to ignore read-only fields class UserUpdateSchema(User.schema.as_marshmallow_schema()): class Meta: - dump_only = ('nick', 'password',) + dump_only = ("nick", "password") user_update_schema = UserUpdateSchema() @@ -108,7 +120,7 @@ class Meta: # Define a custom marshmallow schema from User document to exclude password field class UserNoPassSchema(User.schema.as_marshmallow_schema()): class Meta: - exclude = ('password',) + exclude = ("password",) user_no_pass_schema = UserNoPassSchema() @@ -121,14 +133,14 @@ def dump_user_no_pass(u): # Define a custom marshmallow schema from User document to expose only password field class ChangePasswordSchema(User.schema.as_marshmallow_schema()): class Meta: - fields = ('password',) - required = ('password',) + fields = ("password",) + required = ("password",) change_password_schema = ChangePasswordSchema() -@app.route('/', methods=['GET']) +@app.route("/", methods=["GET"]) def root(request): return """

Umongo flask example


@@ -151,7 +163,7 @@ def _to_objid(data): def _nick_or_id_lookup(nick_or_id): - return {'$or': [{'nick': nick_or_id}, {'_id': _to_objid(nick_or_id)}]} + return {"$or": [{"nick": nick_or_id}, {"_id": _to_objid(nick_or_id)}]} class Error(Exception): @@ -165,98 +177,112 @@ def error(request, failure): return data -@app.route('/users/', methods=['GET']) +@app.route("/users/", methods=["GET"]) @locale_from_request @inlineCallbacks def get_user(request, nick_or_id): user = yield User.find_one(_nick_or_id_lookup(nick_or_id)) if not user: - raise Error(404, 'Not found') + raise Error(404, "Not found") returnValue(jsonify(request, dump_user_no_pass(user))) -@app.route('/users/', methods=['PATCH']) +@app.route("/users/", methods=["PATCH"]) @locale_from_request @inlineCallbacks def update_user(request, nick_or_id): payload = get_json(request) if payload is None: - raise Error(400, 'Request body must be json with Content-type: application/json') + raise Error( + 400, + "Request body must be json with Content-type: application/json", + ) user = yield User.find_one(_nick_or_id_lookup(nick_or_id)) if not user: - raise Error(404, 'Not found') + raise Error(404, "Not found") try: data = user_update_schema.load(payload) user.update(data) yield user.commit() except ValidationError as ve: - raise Error(400, jsonify(request, message=ve.args[0])) + raise Error(400, jsonify(request, message=ve.args[0])) from ve returnValue(jsonify(request, dump_user_no_pass(user))) -@app.route('/users/', methods=['DELETE']) +@app.route("/users/", methods=["DELETE"]) @locale_from_request @inlineCallbacks def delete_user(request, nick_or_id): user = yield User.find_one(_nick_or_id_lookup(nick_or_id)) if not user: - raise Error(404, 'Not Found') + raise Error(404, "Not Found") try: yield user.delete() except ValidationError as ve: - raise Error(400, jsonify(message=ve.args[0])) - returnValue('Ok') + raise Error(400, jsonify(message=ve.args[0])) from ve + returnValue("Ok") -@app.route('/users//password', methods=['PUT']) +@app.route("/users//password", methods=["PUT"]) @locale_from_request @inlineCallbacks def change_password_user(request, nick_or_id): payload = get_json(request) if payload is None: - raise Error(400, 'Request body must be json with Content-type: application/json') + raise Error( + 400, + "Request body must be json with Content-type: application/json", + ) user = yield User.find_one(_nick_or_id_lookup(nick_or_id)) if not user: - raise Error(404, 'Not found') + raise Error(404, "Not found") try: data = change_password_schema.load(payload) - user.password = data['password'] + user.password = data["password"] yield user.commit() except ValidationError as ve: - raise Error(400, jsonify(request, message=ve.args[0])) + raise Error(400, jsonify(request, message=ve.args[0])) from ve returnValue(jsonify(request, dump_user_no_pass(user))) -@app.route('/users', methods=['GET']) +@app.route("/users", methods=["GET"]) @locale_from_request @inlineCallbacks def list_users(request): - page = int(request.args.get('page', 1)) + page = int(request.args.get("page", 1)) users = yield User.find(limit=10, skip=(page - 1) * 10) - returnValue(jsonify(request, { - '_total': (yield User.count()), - '_page': page, - '_per_page': 10, - '_items': [dump_user_no_pass(u) for u in users] - })) - - -@app.route('/users', methods=['POST']) + returnValue( + jsonify( + request, + { + "_total": (yield User.count()), + "_page": page, + "_per_page": 10, + "_items": [dump_user_no_pass(u) for u in users], + }, + ), + ) + + +@app.route("/users", methods=["POST"]) @locale_from_request @inlineCallbacks def create_user(request): payload = get_json(request) if payload is None: - raise Error(400, 'Request body must be json with Content-type: application/json') + raise Error( + 400, + "Request body must be json with Content-type: application/json", + ) try: user = User(**payload) yield user.commit() except ValidationError as ve: - raise Error(400, jsonify(request, message=ve.args[0])) + raise Error(400, jsonify(request, message=ve.args[0])) from ve returnValue(jsonify(request, dump_user_no_pass(user))) -if __name__ == '__main__': +if __name__ == "__main__": reactor.callWhenRunning(populate_db) - app.run('localhost', 5000) + app.run("localhost", 5000) diff --git a/examples/klein/klein_babel.py b/examples/klein/klein_babel.py index 9a531c52..d9d32648 100644 --- a/examples/klein/klein_babel.py +++ b/examples/klein/klein_babel.py @@ -2,13 +2,13 @@ import re from functools import wraps -from twisted.python import context -from babel import support +from babel import support +from twisted.python import context -locale_delim_re = re.compile(r'[_-]') +locale_delim_re = re.compile(r"[_-]") accept_re = re.compile( - r'''( # media-range capturing-parenthesis + r"""( # media-range capturing-parenthesis [^\s;,]+ # type/subtype (?:[ \t]*;[ \t]* # ";" (?: # parameter non-capturing-parenthesis @@ -22,7 +22,9 @@ (\d*(?:\.\d+)?) # qvalue capturing-parentheses [^,]* # "extension" accept params: who cares? )? # accept params are optional - ''', re.VERBOSE) + """, + re.VERBOSE, +) def parse_accept_header(header): @@ -38,8 +40,8 @@ def parse_accept_header(header): return result -def select_locale_by_request(request, default='en'): - accept_language = request.getHeader('ACCEPT-LANGUAGE') +def select_locale_by_request(request, default="en"): + accept_language = request.getHeader("ACCEPT-LANGUAGE") if not accept_language: return default @@ -54,18 +56,21 @@ def select_locale_by_request(request, default='en'): def locale_from_request(fn): - @wraps(fn) def wrapper(request, *args, **kwargs): locale = select_locale_by_request(request) translations = support.Translations.load( - 'translations', locales=locale, domain='messages') - ctx = {'locale': locale, 'translations': translations} + "translations", + locales=locale, + domain="messages", + ) + ctx = {"locale": locale, "translations": translations} return context.call(ctx, fn, request, *args, **kwargs) return wrapper def gettext(string): - return context.get( - 'translations', default=support.NullTranslations()).gettext(string) + return context.get("translations", default=support.NullTranslations()).gettext( + string, + ) diff --git a/messages.pot b/messages.pot index 3c0f8984..42a6282b 100644 --- a/messages.pot +++ b/messages.pot @@ -1,186 +1,81 @@ # Translations template for umongo. -# Copyright (C) 2021 Scille SAS and contributors +# Copyright (C) 2025 Scille SAS and contributors # This file is distributed under the same license as the umongo project. -# Jérôme Lafréchoux , 2021. +# FIRST AUTHOR Emmanuel Leblond , 2016. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: umongo 3.0.0b14\n" +"Project-Id-Version: umongo 4.0.0\n" "Report-Msgid-Bugs-To: jerome@jolimont.fr\n" -"POT-Creation-Date: 2021-01-04 15:53+0100\n" +"POT-Creation-Date: 2025-10-07 00:07+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Jérôme Lafréchoux \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.7.0\n" +"Generated-By: Babel 2.17.0\n" -#: umongo/abstract.py:103 +#: src/umongo/abstract.py:95 msgid "Field value must be unique." msgstr "" -#: umongo/abstract.py:104 +#: src/umongo/abstract.py:96 +#, python-brace-format msgid "Values of fields {fields} must be unique together." msgstr "" -#: umongo/data_objects.py:155 +#: src/umongo/data_objects.py:157 +#, python-brace-format msgid "Reference not found for document {document}." msgstr "" -#: umongo/data_proxy.py:65 -msgid "{cls}: unknown \"{key}\" field found in DB." +#: src/umongo/data_proxy.py:65 +#, python-format +msgid "%s: unknown \"%s\" field found in DB." msgstr "" -#: umongo/data_proxy.py:170 +#: src/umongo/data_proxy.py:171 msgid "Missing data for required field." msgstr "" -#: umongo/fields.py:347 +#: src/umongo/fields.py:350 +#, python-brace-format msgid "DBRef must be on collection `{collection}`." msgstr "" -#: umongo/fields.py:352 umongo/fields.py:363 +#: src/umongo/fields.py:358 src/umongo/fields.py:373 +#, python-brace-format msgid "`{document}` reference expected." msgstr "" -#: umongo/fields.py:360 umongo/fields.py:404 +#: src/umongo/fields.py:368 src/umongo/fields.py:416 msgid "Cannot reference a document that has not been created yet." msgstr "" -#: umongo/fields.py:386 umongo/fields.py:483 +#: src/umongo/fields.py:398 src/umongo/fields.py:503 +#, python-brace-format msgid "Unknown document `{document}`." msgstr "" -#: umongo/fields.py:408 umongo/marshmallow_bonus.py:65 +#: src/umongo/fields.py:422 src/umongo/marshmallow_bonus.py:61 msgid "Generic reference must have `id` and `cls` fields." msgstr "" -#: umongo/fields.py:412 umongo/marshmallow_bonus.py:69 +#: src/umongo/fields.py:427 src/umongo/marshmallow_bonus.py:66 msgid "Invalid `id` field." msgstr "" -#: umongo/fields.py:415 umongo/marshmallow_bonus.py:63 +#: src/umongo/fields.py:430 src/umongo/marshmallow_bonus.py:58 msgid "Invalid value for generic reference field." msgstr "" -#: umongo/marshmallow_bonus.py:29 +#: src/umongo/marshmallow_bonus.py:28 msgid "Invalid ObjectId." msgstr "" - -# Marshmallow fields # - -msgid "Missing data for required field." -msgstr "" - -msgid "Invalid input type." -msgstr "" - -msgid "Field may not be null." -msgstr "" - -msgid "Invalid value." -msgstr "" - -msgid "Invalid type." -msgstr "" - -msgid "Not a valid list." -msgstr "" - -msgid "Not a valid string." -msgstr "" - -msgid "Not a valid number." -msgstr "" - -msgid "Not a valid integer." -msgstr "" - -msgid "Special numeric values are not permitted." -msgstr "" - -msgid "Not a valid boolean." -msgstr "" - -msgid "Cannot format string with given data." -msgstr "" - -msgid "Not a valid datetime." -msgstr "" - -msgid "\"{input}\" cannot be formatted as a datetime." -msgstr "" - -msgid "Not a valid time." -msgstr "" - -msgid "\"{input}\" cannot be formatted as a time." -msgstr "" - -msgid "Not a valid date." -msgstr "" - -msgid "\"{input}\" cannot be formatted as a date." -msgstr "" - -msgid "Not a valid period of time." -msgstr "" - -msgid "{input!r} cannot be formatted as a timedelta." -msgstr "" - -msgid "Not a valid mapping type." -msgstr "" - -msgid "Not a valid URL." -msgstr "" - -msgid "Not a valid email address." -msgstr "" - -# Marshmallow validate # -msgid "Not a valid URL." -msgstr "" - -msgid "Not a valid email address." -msgstr "" - -msgid "Must be at least {min}." -msgstr "" - -msgid "Must be at most {max}." -msgstr "" - -msgid "Must be between {min} and {max}." -msgstr "" - -msgid "Shorter than minimum length {min}." -msgstr "" - -msgid "Longer than maximum length {max}." -msgstr "" - -msgid "Length must be between {min} and {max}." -msgstr "" - -msgid "Length must be {equal}." -msgstr "" - -msgid "Must be equal to {other}." -msgstr "" - -msgid "String does not match expected pattern." -msgstr "" - -msgid "Invalid input." -msgstr "" - -msgid "Not a valid choice." -msgstr "" - -msgid "One or more of the choices you made was not acceptable." +#: tests/test_i18n.py:26 +msgid "hello" msgstr "" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..a7387ffe --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,135 @@ +[project] +name = "umongo" +version = "4.0.0b4" +description = "sync/async MongoDB ODM" +readme = "README.rst" +license = { file = "LICENSE" } +authors = [ + { name = "Emmanuel Leblond", email = "emmanuel.leblond@gmail.com" }, + { name = "Jérôme Lafréchoux", email = "jerome@jolimont.fr" }, +] +maintainers = [ + { name = "Jérôme Lafréchoux", email = "jerome@jolimont.fr" }, +] +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3 :: Only', +] +requires-python = ">=3.10" +dependencies = [ + "marshmallow>=4.0.1,<5.0", + "pymongo>=3.7.0", +] + +[project.urls] +Changelog = "https://umongo.readthedocs.io/en/latest/history.html" +Issues = "https://github.com/Scille/umongo/issues" +Source = "https://github.com/Scille/umongo/" + +[project.optional-dependencies] +motor = [ + 'motor>=3.1.1', +] +txmongo = [ + 'txmongo>=19.2.0', +] +mongomock = [ + 'mongomock', +] +tests = [ + "pytest", + "pytest-cov", +] +dev = ["umongo[tests]", "tox", "pre-commit>=4.3,<5.0"] +docs = [ + "sphinx==8.2.3", +] + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.sdist] +include = [ + "docs/", + "tests/", + "CHANGELOG.rst", + "CONTRIBUTING.rst", + "tox.ini", +] +exclude = ["docs/_build/"] + +[tool.ruff] +fix = true + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +# use all checks available in ruff except the ones explicitly ignored below +select = ["ALL"] +ignore = [ + "A001", # "variable name shadows a Python standard-library module" + "A002", # "argument name shadows a Python standard-library module" + "ANN", # skip annotation checks + "ARG", # unused arguments are common w/ interfaces + "COM", # let formatter take care of commas + "C901", # don't enforce complexity level + "D", # don't require docstrings + "DTZ001", # allow calling `datetime.datetime()` without a `tzinfo` argument + "EM", # allow string messages in exceptions + "FBT002", # allow boolean default positional argument + "FIX", # allow "FIX" comments in code + "INP001", # allow Python files outside of packages + "N805", # allow first method argument not to be self (can be cls) + "N806", # allow uppercase variable names for variables that are classes + "N816", # allow mixedcase variable names for variables in global scope + "PERF203", # allow try-except within loops + "PLR0912", # "Too many branches" + "PLR0913", # "Too many arguments" + "PLR0915", # "Too many statements" + "PLR2004", # "Magic value used in comparison" + "PLW0642", # allow reassigning `cls` variable in class method" + "PLW1641", # allow objects not implementing `__hash__` method + "PLW2901", # `for` loop variable `base` overwritten by assignment target + "RET504", # Unnecessary assignment" + "RUF012", # allow mutable class variables + "S", # allow asserts + "SIM102", # Sometimes nested ifs are more readable than if...and... + "SIM105", # "Use `contextlib.suppress(...)` instead of `try`-`except`-`pass`" + "SIM108", # sometimes if-else is more readable than a ternary + "SLF001", # allow private attribute access + "TD", # allow TODO comments to be whatever we want + "TRY003", # allow long messages passed to exceptions +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = [ + "B018", # allow useless expressions + "PT007", # ignore false positives due to https://github.com/astral-sh/ruff/issues/14743 + "TID252", # ignore relative imports from parent modules" +] +"examples/*" = [ + "BLE001", # allow blind exception catching + "EXE001", # allow shebang in non-executable file + "T", # allow prints +] + +[tool.ruff.lint.isort] +section-order = ["future", "standard-library", "testing", "marshmallow", "mongodb", "third-party", "first-party", "local-folder"] + +[tool.ruff.lint.isort.sections] +testing = ["pytest"] +marshmallow = ["marshmallow"] +mongodb = ["bson", "pymongo", "motor", "txmongo", "mongomock"] + +[tool.pytest.ini_options] +norecursedirs = ".git .tox docs env" diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index cd5180d2..00000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,8 +0,0 @@ -bumpversion -wheel -flake8 -tox -coverage -Sphinx -pytest -pytest-cov diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 5667b7aa..00000000 --- a/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[bumpversion] -current_version = 3.1.0 -commit = True -tag = True - -[bumpversion:file:setup.py] -search = version='{current_version}' -replace = version='{new_version}' - -[bumpversion:file:umongo/__init__.py] -search = __version__ = '{current_version}' -replace = __version__ = '{new_version}' - -[wheel] -universal = 1 - -[flake8] -ignore = E127,E128,W504 -max-line-length = 100 -per-file-ignores = - docs/conf.py: E265 - -[extract_messages] -project = umongo -copyright_holder = Scille SAS and contributors -msgid_bugs_address = jerome@jolimont.fr -output_file = messages.pot diff --git a/setup.py b/setup.py deleted file mode 100755 index aa5b05c3..00000000 --- a/setup.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - - -with open('README.rst', 'rb') as readme_file: - readme = readme_file.read().decode('utf8') - -with open('HISTORY.rst', 'rb') as history_file: - history = history_file.read().decode('utf8') - -requirements = [ - "marshmallow>=3.10.0", - "pymongo>=3.7.0", -] - -setup( - name='umongo', - version='3.1.0', - description="sync/async MongoDB ODM, yes.", - long_description=readme + '\n\n' + history, - author="Emmanuel Leblond, Jérôme Lafréchoux", - author_email='jerome@jolimont.fr', - url='https://github.com/touilleMan/umongo', - packages=['umongo', 'umongo.frameworks'], - include_package_data=True, - python_requires='>=3.7', - install_requires=requirements, - extras_require={ - 'motor': ['motor>=2.0,<3.0'], - 'txmongo': ['txmongo>=19.2.0'], - 'mongomock': ['mongomock'], - }, - license="MIT", - zip_safe=False, - keywords='umongo mongodb pymongo txmongo motor mongomock asyncio twisted', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3 :: Only', - ], -) diff --git a/src/umongo/__init__.py b/src/umongo/__init__.py new file mode 100644 index 00000000..cdceb882 --- /dev/null +++ b/src/umongo/__init__.py @@ -0,0 +1,53 @@ +from marshmallow import ValidationError, missing + +from . import fields, validate +from .data_objects import Reference +from .document import ( + Document, + post_dump, + post_load, + pre_dump, + pre_load, + validates_schema, +) +from .embedded_document import EmbeddedDocument +from .exceptions import ( + AlreadyCreatedError, + DeleteError, + NoneReferenceError, + NotCreatedError, + UMongoError, + UnknownFieldInDBError, + UpdateError, +) +from .expose_missing import ExposeMissing, RemoveMissingSchema +from .i18n import set_gettext +from .instance import Instance +from .mixin import MixinDocument + +__all__ = ( + "AlreadyCreatedError", + "DeleteError", + "Document", + "EmbeddedDocument", + "ExposeMissing", + "Instance", + "MixinDocument", + "NoneReferenceError", + "NotCreatedError", + "Reference", + "RemoveMissingSchema", + "UMongoError", + "UnknownFieldInDBError", + "UpdateError", + "ValidationError", + "fields", + "missing", + "post_dump", + "post_load", + "pre_dump", + "pre_load", + "set_gettext", + "validate", + "validates_schema", +) diff --git a/umongo/abstract.py b/src/umongo/abstract.py similarity index 66% rename from umongo/abstract.py rename to src/umongo/abstract.py index ef92231e..99420dc3 100644 --- a/umongo/abstract.py +++ b/src/umongo/abstract.py @@ -1,16 +1,16 @@ import marshmallow as ma -from .expose_missing import RemoveMissingSchema from .exceptions import DocumentDefinitionError -from .i18n import gettext as _, N_ - +from .expose_missing import RemoveMissingSchema +from .i18n import N_ +from .i18n import gettext as _ __all__ = ( - 'BaseSchema', - 'BaseMarshmallowSchema', - 'BaseField', - 'BaseValidator', - 'BaseDataObject' + "BaseDataObject", + "BaseField", + "BaseMarshmallowSchema", + "BaseSchema", + "BaseValidator", ) @@ -22,30 +22,23 @@ def __getitem__(self, name): class BaseMarshmallowSchema(RemoveMissingSchema): """Base schema for pure marshmallow schemas""" - class Meta: - ordered = True class BaseSchema(ma.Schema): - """ - All schema used in umongo should inherit from this base schema - """ + """All schema used in umongo should inherit from this base schema""" + # This class attribute is overriden by the builder upon registration # to let the template set the base marshmallow schema class. # It may be overriden in Template classes. MA_BASE_SCHEMA_CLS = BaseMarshmallowSchema - class Meta: - ordered = True - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.error_messages = I18nErrorDict(self.error_messages) self._ma_schema = None def map_to_field(self, func): - """ - Apply a function to every field in the schema + """Apply a function to every field in the schema >>> def func(mongo_path, path, field): ... pass @@ -53,7 +46,7 @@ def map_to_field(self, func): for name, field in self.fields.items(): mongo_path = field.attribute or name func(mongo_path, name, field) - if hasattr(field, 'map_to_field'): + if hasattr(field, "map_to_field"): field.map_to_field(mongo_path, name, func) def as_marshmallow_schema(self): @@ -64,23 +57,22 @@ def as_marshmallow_schema(self): # Create schema if not found in cache nmspc = { - name: field.as_marshmallow_field() - for name, field in self.fields.items() + name: field.as_marshmallow_field() for name, field in self.fields.items() } - name = 'Marshmallow%s' % type(self).__name__ - m_schema = type(name, (self.MA_BASE_SCHEMA_CLS, ), nmspc) + name = f"Marshmallow{type(self).__name__}" + m_schema = type(name, (self.MA_BASE_SCHEMA_CLS,), nmspc) # Add i18n support to the schema # We can't use I18nErrorDict here because __getitem__ is not called # when error_messages is updated with _default_error_messages. m_schema._default_error_messages = { - k: _(v) for k, v in m_schema._default_error_messages.items()} + k: _(v) for k, v in m_schema._default_error_messages.items() + } self._ma_schema = m_schema return m_schema class BaseField(ma.fields.Field): - """ - All fields used in umongo should inherit from this base field. + """All fields used in umongo should inherit from this base field. ============================== =============== Enabled flags resulting index @@ -100,27 +92,27 @@ class BaseField(ma.fields.Field): """ default_error_messages = { - 'unique': N_('Field value must be unique.'), - 'unique_compound': N_('Values of fields {fields} must be unique together.') + "unique": N_("Field value must be unique."), + "unique_compound": N_("Values of fields {fields} must be unique together."), } - MARSHMALLOW_ARGS_PREFIX = 'marshmallow_' + MARSHMALLOW_ARGS_PREFIX = "marshmallow_" def __init__(self, *args, io_validate=None, unique=False, instance=None, **kwargs): - if 'missing' in kwargs: + if "missing" in kwargs: raise DocumentDefinitionError( "uMongo doesn't use `missing` argument, use `default` " - "instead and `marshmallow_missing`/`marshmallow_default` " + "instead and `marshmallow_load_default`/`marshmallow_dump_default` " "to tell `as_marshmallow_field` to use a custom value when " - "generating pure Marshmallow field." + "generating pure Marshmallow field.", ) - if 'default' in kwargs: - kwargs['missing'] = kwargs['default'] + if "default" in kwargs: + kwargs["load_default"] = kwargs["dump_default"] = kwargs.pop("default") # Store attributes prefixed with marshmallow_ to use them when # creating pure marshmallow Schema self._ma_kwargs = { - key[len(self.MARSHMALLOW_ARGS_PREFIX):]: val + key[len(self.MARSHMALLOW_ARGS_PREFIX) :]: val for key, val in kwargs.items() if key.startswith(self.MARSHMALLOW_ARGS_PREFIX) } @@ -132,10 +124,8 @@ def __init__(self, *args, io_validate=None, unique=False, instance=None, **kwarg super().__init__(*args, **kwargs) - if hasattr(self, 'missing'): - self._ma_kwargs.setdefault('missing', self.default) - if hasattr(self, 'default'): - self._ma_kwargs.setdefault('default', self.default) + self._ma_kwargs.setdefault("dump_default", self.dump_default) + self._ma_kwargs.setdefault("load_default", self.dump_default) # Overwrite error_messages to handle i18n translation self.error_messages = I18nErrorDict(self.error_messages) @@ -148,38 +138,36 @@ def __init__(self, *args, io_validate=None, unique=False, instance=None, **kwarg self.instance = instance def __repr__(self): - return ('' - .format(ClassName=self.__class__.__name__, self=self)) + return ( + f"" + ) def _validate_missing(self, value): # Overwrite marshmallow.Field._validate_missing given it also checks # for missing required fields (this is done at commit time in umongo # using `DataProxy.required_validate`). - if value is None and getattr(self, 'allow_none', False) is False: - self.fail('null') + if value is None and getattr(self, "allow_none", False) is False: + self.fail("null") def serialize_to_mongo(self, obj): - if obj is None and getattr(self, 'allow_none', False) is True: + if obj is None and getattr(self, "allow_none", False) is True: return None if obj is ma.missing: return ma.missing return self._serialize_to_mongo(obj) - # def serialize_to_mongo_update(self, path, obj): - # return self._serialize_to_mongo(attr, obj=obj, update=update) - def deserialize_from_mongo(self, value): - if value is None and getattr(self, 'allow_none', False) is True: + if value is None and getattr(self, "allow_none", False) is True: return None return self._deserialize_from_mongo(value) @@ -193,21 +181,27 @@ def _extract_marshmallow_field_params(self): params = { attribute: getattr(self, attribute) for attribute in ( - 'validate', 'required', 'allow_none', - 'load_only', 'dump_only', 'error_messages' + "validate", + "required", + "allow_none", + "load_only", + "dump_only", + "error_messages", ) } # Override uMongo attributes with marshmallow_ prefixed attributes params.update(self._ma_kwargs) return params - def as_marshmallow_field(self): + def as_marshmallow_field(self): # noqa: RET503 (no explicit return) """Return a pure-marshmallow version of this field""" field_kwargs = self._extract_marshmallow_field_params() # Retrieve the marshmallow class we inherit from for m_class in type(self).mro(): - if (not issubclass(m_class, BaseField) and - issubclass(m_class, ma.fields.Field)): + if not issubclass(m_class, BaseField) and issubclass( + m_class, + ma.fields.Field, + ): m_field = m_class(**field_kwargs, metadata=self.metadata) # Add i18n support to the field m_field.error_messages = I18nErrorDict(m_field.error_messages) @@ -216,9 +210,7 @@ def as_marshmallow_field(self): class BaseValidator(ma.validate.Validator): - """ - All validators in umongo should inherit from this base validator. - """ + """All validators in umongo should inherit from this base validator.""" def __init__(self, *args, **kwargs): self._error = None @@ -234,15 +226,13 @@ def error(self, value): class BaseDataObject: - """ - All data objects in umongo should inherit from this base data object. - """ + """All data objects in umongo should inherit from this base data object.""" def is_modified(self): - raise NotImplementedError() + raise NotImplementedError def clear_modified(self): - raise NotImplementedError() + raise NotImplementedError @classmethod def build_from_mongo(cls, data): diff --git a/umongo/builder.py b/src/umongo/builder.py similarity index 68% rename from umongo/builder.py rename to src/umongo/builder.py index 75b025a1..654db5e5 100644 --- a/umongo/builder.py +++ b/src/umongo/builder.py @@ -4,21 +4,24 @@ :class:`umongo.instance.BaseInstance` by generating an :class:`umongo.document.Implementation`. """ + import re from copy import copy import marshmallow as ma +from . import fields from .abstract import BaseSchema -from .template import Template, Implementation from .data_proxy import data_proxy_factory -from .document import DocumentTemplate, DocumentOpts, DocumentImplementation +from .document import DocumentImplementation, DocumentOpts, DocumentTemplate from .embedded_document import ( - EmbeddedDocumentTemplate, EmbeddedDocumentOpts, EmbeddedDocumentImplementation) -from .mixin import MixinDocumentTemplate, MixinDocumentOpts, MixinDocumentImplementation + EmbeddedDocumentImplementation, + EmbeddedDocumentOpts, + EmbeddedDocumentTemplate, +) from .exceptions import DocumentDefinitionError, NotRegisteredDocumentError -from . import fields - +from .mixin import MixinDocumentImplementation, MixinDocumentOpts, MixinDocumentTemplate +from .template import Implementation, Template TEMPLATE_IMPLEMENTATION_MAPPING = { DocumentTemplate: DocumentImplementation, @@ -40,27 +43,27 @@ def _get_base_template_cls(template): return EmbeddedDocumentTemplate if issubclass(template, MixinDocumentTemplate): return MixinDocumentTemplate - assert False + raise AssertionError def camel_to_snake(name): - tmp_str = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', tmp_str).lower() + tmp_str = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", tmp_str).lower() def _is_child(template, base_tmpl_cls): """Return true if the (embedded) document has a concrete parent""" return any( - b for b in template.__bases__ - if issubclass(b, base_tmpl_cls) and - b is not base_tmpl_cls and - ('Meta' not in b.__dict__ or not getattr(b.Meta, 'abstract', False)) + b + for b in template.__bases__ + if issubclass(b, base_tmpl_cls) + and b is not base_tmpl_cls + and ("Meta" not in b.__dict__ or not getattr(b.Meta, "abstract", False)) ) def _on_need_add_id_field(bases, fields_dict): - """ - If the given fields make no reference to `_id`, add an `id` field + """If the given fields make no reference to `_id`, add an `id` field (type ObjectId, dump_only=True, attribute=`_id`) to handle it """ @@ -69,7 +72,7 @@ def find_id_field(fields_dict): # Skip fake fields present in schema (e.g. `post_load` decorated function) if not isinstance(field, ma.fields.Field): continue - if (name == '_id' and not field.attribute) or field.attribute == '_id': + if (name == "_id" and not field.attribute) or field.attribute == "_id": return name return None @@ -86,20 +89,19 @@ def find_id_field(fields_dict): return name # No id field found, add a default one - fields_dict['id'] = fields.ObjectIdField(attribute='_id', dump_only=True) - return 'id' + fields_dict["id"] = fields.ObjectIdField(attribute="_id", dump_only=True) + return "id" def _collect_schema_attrs(template): - """ - Split dict between schema fields and non-fields elements and retrieve + """Split dict between schema fields and non-fields elements and retrieve marshmallow tags if any. """ schema_fields = {} schema_non_fields = {} nmspc = {} for key, item in template.__dict__.items(): - if hasattr(item, '__marshmallow_hook__'): + if hasattr(item, "__marshmallow_hook__"): # Decorated special functions (e.g. `post_load`) schema_non_fields[key] = item elif isinstance(item, ma.fields.Field): @@ -113,13 +115,13 @@ def _collect_schema_attrs(template): class BaseBuilder: - """ - A builder connect a :class:`umongo.document.Template` with a + """A builder connect a :class:`umongo.document.Template` with a :class:`umongo.instance.BaseInstance` by generating an :class:`umongo.document.Implementation`. .. note:: This class should not be used directly, it should be inherited by - concrete implementations such as :class:`umongo.frameworks.pymongo.PyMongoBuilder` + concrete implementations such as + :class:`umongo.frameworks.pymongo.PyMongoBuilder` """ BASE_DOCUMENT_CLS = None @@ -134,14 +136,15 @@ def __init__(self, instance): } def _convert_bases(self, bases): - "Replace template parents by their implementation inside this instance" + """Replace template parents by their implementation inside this instance""" converted_bases = [] for base in bases: - assert not issubclass(base, Implementation), \ - 'Document cannot inherit of implementations' + assert not issubclass(base, Implementation), ( + "Document cannot inherit of implementations" + ) if issubclass(base, Template): if base not in self._templates_lookup: - raise NotRegisteredDocumentError('Unknown document `%r`' % base) + raise NotRegisteredDocumentError(f'Unknown document "{base!r}"') converted_bases.append(self._templates_lookup[base]) else: converted_bases.append(base) @@ -167,55 +170,58 @@ def _build_schema(self, template, schema_bases, schema_fields, schema_non_fields schema_nmspc = {} schema_nmspc.update(schema_fields) schema_nmspc.update(schema_non_fields) - schema_nmspc['MA_BASE_SCHEMA_CLS'] = template.MA_BASE_SCHEMA_CLS - return type('%sSchema' % template.__name__, schema_bases, schema_nmspc) + schema_nmspc["MA_BASE_SCHEMA_CLS"] = template.MA_BASE_SCHEMA_CLS + return type("f{template.__name__}Schema", schema_bases, schema_nmspc) def _build_document_opts(self, template, bases, is_child): base_tmpl_cls = _get_base_template_cls(template) base_impl_cls = TEMPLATE_IMPLEMENTATION_MAPPING[base_tmpl_cls] base_opts_cls = TEMPLATE_OPTIONS_MAPPING[base_tmpl_cls] kwargs = {} - kwargs['instance'] = self.instance - kwargs['template'] = template + kwargs["instance"] = self.instance + kwargs["template"] = template if base_tmpl_cls in (DocumentTemplate, EmbeddedDocumentTemplate): - meta = template.__dict__.get('Meta') - kwargs['abstract'] = getattr(meta, 'abstract', False) - kwargs['is_child'] = is_child - kwargs['strict'] = getattr(meta, 'strict', True) + meta = template.__dict__.get("Meta") + kwargs["abstract"] = getattr(meta, "abstract", False) + kwargs["is_child"] = is_child + kwargs["strict"] = getattr(meta, "strict", True) if base_tmpl_cls is DocumentTemplate: - collection_name = getattr(meta, 'collection_name', None) + collection_name = getattr(meta, "collection_name", None) # Handle option inheritance and integrity checks for base in bases: if not issubclass(base, base_impl_cls): continue popts = base.opts - if kwargs['abstract'] and not popts.abstract: + if kwargs["abstract"] and not popts.abstract: raise DocumentDefinitionError( - "Abstract document should have all its parents abstract") + "Abstract document should have all its parents abstract", + ) if base_tmpl_cls is DocumentTemplate: if popts.collection_name: if collection_name: raise DocumentDefinitionError( - "Cannot redefine collection_name in a child, use abstract instead") + "Cannot redefine collection_name in a child, " + "use abstract instead", + ) collection_name = popts.collection_name if base_tmpl_cls is DocumentTemplate: if collection_name: - if kwargs['abstract']: + if kwargs["abstract"]: raise DocumentDefinitionError( - 'Abstract document cannot define collection_name') - elif not kwargs['abstract']: + "Abstract document cannot define collection_name", + ) + elif not kwargs["abstract"]: # Determine the collection name from the class name collection_name = camel_to_snake(template.__name__) - kwargs['collection_name'] = collection_name + kwargs["collection_name"] = collection_name return base_opts_cls(**kwargs) def build_from_template(self, template): - """ - Generate a :class:`umongo.document.DocumentImplementation` for this + """Generate a :class:`umongo.document.DocumentImplementation` for this instance from the given :class:`umongo.document.DocumentTemplate`. """ base_tmpl_cls = _get_base_template_cls(template) @@ -227,31 +233,39 @@ def build_from_template(self, template): # Build opts opts = self._build_document_opts(template, bases, is_child) - nmspc['opts'] = opts + nmspc["opts"] = opts # Create schema by retrieving inherited schema classes schema_bases = tuple( - base.Schema for base in bases - if issubclass(base, Implementation) and hasattr(base, 'Schema') + base.Schema + for base in bases + if issubclass(base, Implementation) and hasattr(base, "Schema") ) if not schema_bases: - schema_bases = (BaseSchema, ) + schema_bases = (BaseSchema,) if base_tmpl_cls is DocumentTemplate: - nmspc['pk_field'] = _on_need_add_id_field(schema_bases, schema_fields) + nmspc["pk_field"] = _on_need_add_id_field(schema_bases, schema_fields) if base_tmpl_cls is not MixinDocumentTemplate: if is_child: - schema_fields['cls'] = fields.StringField( - attribute='_cls', default=name, dump_only=True + schema_fields["cls"] = fields.StringField( + attribute="_cls", + default=name, + dump_only=True, ) - schema_cls = self._build_schema(template, schema_bases, schema_fields, schema_non_fields) - nmspc['Schema'] = schema_cls + schema_cls = self._build_schema( + template, + schema_bases, + schema_fields, + schema_non_fields, + ) + nmspc["Schema"] = schema_cls schema = schema_cls() - nmspc['schema'] = schema + nmspc["schema"] = schema if base_tmpl_cls is not MixinDocumentTemplate: - nmspc['DataProxy'] = data_proxy_factory(name, schema, strict=opts.strict) + nmspc["DataProxy"] = data_proxy_factory(name, schema, strict=opts.strict) # Add field names set as class attribute - nmspc['_fields'] = set(schema.fields.keys()) + nmspc["_fields"] = set(schema.fields.keys()) implementation = type(name, bases, nmspc) self._templates_lookup[template] = implementation @@ -259,6 +273,9 @@ def build_from_template(self, template): if base_tmpl_cls is not MixinDocumentTemplate: for base in bases: for parent in base.mro(): - if issubclass(parent, base_impl_cls) and parent is not base_impl_cls: + if ( + issubclass(parent, base_impl_cls) + and parent is not base_impl_cls + ): parent.opts.offspring.add(implementation) return implementation diff --git a/umongo/data_objects.py b/src/umongo/data_objects.py similarity index 83% rename from umongo/data_objects.py rename to src/umongo/data_objects.py index df30d052..f567cf8a 100644 --- a/umongo/data_objects.py +++ b/src/umongo/data_objects.py @@ -3,13 +3,11 @@ from .abstract import BaseDataObject, I18nErrorDict from .i18n import N_ - -__all__ = ('List', 'Dict', 'Reference') +__all__ = ("Dict", "List", "Reference") class List(BaseDataObject, list): - - __slots__ = ('inner_field', '_modified') + __slots__ = ("_modified", "inner_field") def __init__(self, inner_field, *args, **kwargs): super().__init__(*args, **kwargs) @@ -69,8 +67,7 @@ def extend(self, iterable): return ret def __repr__(self): - return '' % ( - self.__module__, self.__class__.__name__, list(self)) + return f"" def is_modified(self): if self._modified: @@ -90,8 +87,7 @@ def clear_modified(self): class Dict(BaseDataObject, dict): - - __slots__ = ('key_field', 'value_field', '_modified') + __slots__ = ("_modified", "key_field", "value_field") def __init__(self, key_field, value_field, *args, **kwargs): super().__init__(*args, **kwargs) @@ -128,16 +124,16 @@ def setdefault(self, key, obj=None): def update(self, other): new = { - self.key_field.deserialize(k) if self.key_field else k: - self.value_field.deserialize(v) if self.value_field else v + self.key_field.deserialize(k) + if self.key_field + else k: self.value_field.deserialize(v) if self.value_field else v for k, v in other.items() } super().update(new) self.set_modified() def __repr__(self): - return '' % ( - self.__module__, self.__class__.__name__, dict(self)) + return f"" def is_modified(self): if self._modified: @@ -157,8 +153,9 @@ def clear_modified(self): class Reference: - - error_messages = I18nErrorDict(not_found=N_('Reference not found for document {document}.')) + error_messages = I18nErrorDict( + not_found=N_("Reference not found for document {document}."), + ) def __init__(self, document_cls, pk): self.document_cls = document_cls @@ -166,8 +163,7 @@ def __init__(self, document_cls, pk): self._document = None def fetch(self, no_data=False, force_reload=False, projection=None): - """ - Retrieve from the database the referenced document + """Retrieve from the database the referenced document :param no_data: if True, the caller is only interested in whether the document is present in database. This means the @@ -181,14 +177,14 @@ def fetch(self, no_data=False, force_reload=False, projection=None): @property def exists(self): - """ - Check if the reference document exists in the database. - """ + """Check if the reference document exists in the database.""" raise NotImplementedError def __repr__(self): - return '' % ( - self.__module__, self.__class__.__name__, self.document_cls.__name__, self.pk) + return ( + f"" + ) def __eq__(self, other): if isinstance(other, self.document_cls): @@ -196,5 +192,8 @@ def __eq__(self, other): if isinstance(other, Reference): return self.pk == other.pk and self.document_cls == other.document_cls if isinstance(other, DBRef): - return self.pk == other.id and self.document_cls.collection.name == other.collection + return ( + self.pk == other.id + and self.document_cls.collection.name == other.collection + ) return NotImplemented diff --git a/umongo/data_proxy.py b/src/umongo/data_proxy.py similarity index 77% rename from umongo/data_proxy.py rename to src/umongo/data_proxy.py index 40a34cbf..29cb9fe0 100644 --- a/umongo/data_proxy.py +++ b/src/umongo/data_proxy.py @@ -1,17 +1,18 @@ """umongo BaseDataProxy""" + import marshmallow as ma from .abstract import BaseDataObject from .exceptions import UnknownFieldInDBError from .i18n import gettext as _ - -__all__ = ('data_proxy_factory') +__all__ = [ + "data_proxy_factory", +] class BaseDataProxy: - - __slots__ = ('_data', '_modified_data') + __slots__ = ("_data", "_modified_data") schema = None _fields = None _fields_from_mongo_key = None @@ -49,9 +50,9 @@ def _to_mongo_update(self): else: set_data[name] = val if set_data: - mongo_data['$set'] = set_data + mongo_data["$set"] = set_data if unset_data: - mongo_data['$unset'] = {k: "" for k in unset_data} + mongo_data["$unset"] = dict.fromkeys(unset_data, "") return mongo_data or None def from_mongo(self, data): @@ -59,11 +60,11 @@ def from_mongo(self, data): for key, val in data.items(): try: field = self._fields_from_mongo_key[key] - except KeyError: - raise UnknownFieldInDBError(_( - '{cls}: unknown "{key}" field found in DB.' - .format(key=key, cls=self.__class__.__name__) - )) + except KeyError as exc: + raise UnknownFieldInDBError( + _('%s: unknown "%s" field found in DB.') + % (self.__class__.__name__, key), + ) from exc self._data[key] = field.deserialize_from_mongo(val) self.clear_modified() self._add_missing_fields() @@ -104,8 +105,8 @@ def get(self, name): def set(self, name, value): name, field = self._get_field(name) - if value is None and not getattr(field, 'allow_none', False): - raise ma.ValidationError(field.error_messages['null']) + if value is None and not getattr(field, "allow_none", False): + raise ma.ValidationError(field.error_messages["null"]) if value is not None: value = field._deserialize(value, name, None) field._validate(value) @@ -114,18 +115,18 @@ def set(self, name, value): def delete(self, name): name, field = self._get_field(name) - default = field.default + default = field.dump_default self._data[name] = default() if callable(default) else default self._mark_as_modified(name) def __repr__(self): # Display data in oo world format - return "<%s(%s)>" % (self.__class__.__name__, dict(self.items())) + return f"<{self.__class__.__name__}({dict(self.items())})>" def __eq__(self, other): if isinstance(other, dict): return self._data == other - if hasattr(other, '_data'): + if hasattr(other, "_data"): return self._data == other._data return NotImplemented @@ -135,7 +136,8 @@ def get_modified_fields(self): value_name = field.attribute or name value = self._data[value_name] if value_name in self._modified_data or ( - isinstance(value, BaseDataObject) and value.is_modified()): + isinstance(value, BaseDataObject) and value.is_modified() + ): modified.add(name) return modified @@ -146,10 +148,9 @@ def clear_modified(self): val.clear_modified() def is_modified(self): - return ( - bool(self._modified_data) or - any(isinstance(v, BaseDataObject) and v.is_modified() - for v in self._data.values()) + return bool(self._modified_data) or any( + isinstance(v, BaseDataObject) and v.is_modified() + for v in self._data.values() ) def _add_missing_fields(self): @@ -157,11 +158,10 @@ def _add_missing_fields(self): for name, field in self._fields.items(): mongo_name = field.attribute or name if mongo_name not in self._data: - if hasattr(field, "missing"): - if callable(field.missing): - self._data[mongo_name] = field.missing() - else: - self._data[mongo_name] = field.missing + if callable(field.load_default): + self._data[mongo_name] = field.load_default() + else: + self._data[mongo_name] = field.load_default def required_validate(self): errors = {} @@ -171,7 +171,7 @@ def required_validate(self): errors[name] = [_("Missing data for required field.")] elif value is ma.missing or value is None: continue - elif hasattr(field, '_required_validate'): + elif hasattr(field, "_required_validate"): try: field._required_validate(value) except ma.ValidationError as exc: @@ -183,7 +183,8 @@ def required_validate(self): def items(self): return ( - (key, self._data[field.attribute or key]) for key, field in self._fields.items() + (key, self._data[field.attribute or key]) + for key, field in self._fields.items() ) def keys(self): @@ -194,12 +195,11 @@ def values(self): class BaseNonStrictDataProxy(BaseDataProxy): - """ - This data proxy will accept unknown data comming from mongo and will + """This data proxy will accept unknown data comming from mongo and will return them along with other data when ask. """ - __slots__ = ('_additional_data', ) + __slots__ = ("_additional_data",) def __init__(self, data=None): self._additional_data = {} @@ -224,21 +224,25 @@ def from_mongo(self, data): def data_proxy_factory(basename, schema, strict=True): - """ - Generate a DataProxy from the given schema. + """Generate a DataProxy from the given schema. This way all generic informations (like schema and fields lookups) are kept inside the DataProxy class and it instances are just flyweights. """ - - cls_name = "%sDataProxy" % basename + cls_name = f"{basename}DataProxy" nmspc = { - '__slots__': (), - 'schema': schema, - '_fields': schema.fields, - '_fields_from_mongo_key': {v.attribute or k: v for k, v in schema.fields.items()} + "__slots__": (), + "schema": schema, + "_fields": schema.fields, + "_fields_from_mongo_key": { + v.attribute or k: v for k, v in schema.fields.items() + }, } - data_proxy_cls = type(cls_name, (BaseDataProxy if strict else BaseNonStrictDataProxy, ), nmspc) + data_proxy_cls = type( + cls_name, + (BaseDataProxy if strict else BaseNonStrictDataProxy,), + nmspc, + ) return data_proxy_cls diff --git a/umongo/document.py b/src/umongo/document.py similarity index 70% rename from umongo/document.py rename to src/umongo/document.py index 9f71138d..434a4023 100644 --- a/umongo/document.py +++ b/src/umongo/document.py @@ -1,38 +1,45 @@ """umongo Document""" + from copy import deepcopy -from bson import DBRef import marshmallow as ma from marshmallow import ( - pre_load, post_load, pre_dump, post_dump, validates_schema, # republishing + post_dump, + post_load, + pre_dump, + pre_load, # republishing + validates_schema, ) +from bson import DBRef + +from .data_objects import Reference +from .embedded_document import EmbeddedDocumentImplementation from .exceptions import ( - AlreadyCreatedError, NotCreatedError, NoDBDefinedError, AbstractDocumentError + AbstractDocumentError, + AlreadyCreatedError, + NoDBDefinedError, + NotCreatedError, ) -from .template import Template, MetaImplementation -from .embedded_document import EmbeddedDocumentImplementation -from .data_objects import Reference from .indexes import parse_index - +from .template import MetaImplementation, Template __all__ = ( - 'DocumentTemplate', - 'Document', - 'DocumentOpts', - 'MetaDocumentImplementation', - 'DocumentImplementation', - 'pre_load', - 'post_load', - 'pre_dump', - 'post_dump', - 'validates_schema' + "Document", + "DocumentImplementation", + "DocumentOpts", + "DocumentTemplate", + "MetaDocumentImplementation", + "post_dump", + "post_load", + "pre_dump", + "pre_load", + "validates_schema", ) class DocumentTemplate(Template): - """ - Base class to define a umongo document. + """Base class to define a umongo document. .. note:: Once defined, this class must be registered inside a @@ -50,8 +57,7 @@ class DocumentTemplate(Template): class DocumentOpts: - """ - Configuration for a document. + """Configuration for a document. Should be passed as a Meta class to the :class:`Document` @@ -62,6 +68,7 @@ class Doc(Document): class Meta: abstract = True + assert Doc.opts.abstract == True @@ -74,27 +81,39 @@ class Meta: and can only be inherited collection_name yes Name of the collection to store the document into - is_child no Document inherit of a non-abstract document + is_child no Document inherits a non-abstract + document strict yes Don't accept unknown fields from mongo (default: True) indexes yes List of custom indexes offspring no List of Documents inheriting this one ==================== ====================== =========== """ + def __repr__(self): - return ('<{ClassName}(' - 'instance={self.instance}, ' - 'template={self.template}, ' - 'abstract={self.abstract}, ' - 'collection_name={self.collection_name}, ' - 'is_child={self.is_child}, ' - 'strict={self.strict}, ' - 'indexes={self.indexes}, ' - 'offspring={self.offspring})>' - .format(ClassName=self.__class__.__name__, self=self)) - - def __init__(self, instance, template, collection_name=None, abstract=False, - indexes=None, is_child=True, strict=True, offspring=None): + return ( + f"<{self.__class__.__name__}(" + f"instance={self.instance}, " + f"template={self.template}, " + f"abstract={self.abstract}, " + f"collection_name={self.collection_name}, " + f"is_child={self.is_child}, " + f"strict={self.strict}, " + f"indexes={self.indexes}, " + f"offspring={self.offspring})>" + ) + + def __init__( + self, + instance, + template, + collection_name=None, + abstract=False, + indexes=None, + is_child=True, + strict=True, + offspring=None, + ): self.instance = instance self.template = template self.collection_name = collection_name if not abstract else None @@ -106,39 +125,35 @@ def __init__(self, instance, template, collection_name=None, abstract=False, class MetaDocumentImplementation(MetaImplementation): - def __init__(cls, *args, **kwargs): cls._indexes = None @property def collection(cls): - """ - Return the collection used by this document class - """ + """Return the collection used by this document class""" if cls.opts.abstract: - raise NoDBDefinedError('Abstract document has no collection') + raise NoDBDefinedError("Abstract document has no collection") if cls.opts.instance.db is None: - raise NoDBDefinedError('Instance must be initialized first') + raise NoDBDefinedError("Instance must be initialized first") return cls.opts.instance.db[cls.opts.collection_name] @property def indexes(cls): - """ - Retrieve all indexes (custom defined in meta class, by inheritances + """Retrieve all indexes (custom defined in meta class, by inheritances and unique attributes in fields) """ if cls._indexes is None: - idxs = [] is_child = cls.opts.is_child # First collect parent indexes (including inherited field's unique indexes) for base in cls.mro(): if ( - base is not cls and - issubclass(base, DocumentImplementation) and - # Skip base framework doc classes - hasattr(base, "schema") + base is not cls + and issubclass(base, DocumentImplementation) + and + # Skip base framework doc classes + hasattr(base, "schema") ): idxs += base.indexes @@ -152,21 +167,21 @@ def indexes(cls): # Add _cls to indexes if is_child: - idxs.append(parse_index('_cls')) + idxs.append(parse_index("_cls")) # Finally parse our own fields (i.e. not inherited) for unique indexes def parse_field(mongo_path, path, field): if field.unique: - index = {'unique': True, 'key': [mongo_path]} + index = {"unique": True, "key": [mongo_path]} if not field.required or field.allow_none: - index['sparse'] = True + index["sparse"] = True if is_child: - index['key'].append('_cls') + index["key"].append("_cls") idxs.append(parse_index(index)) for name, field in cls.schema.fields.items(): parse_field(name or field.attribute, name, field) - if hasattr(field, 'map_to_field'): + if hasattr(field, "map_to_field"): field.map_to_field(name or field.attribute, name, parse_field) cls._indexes = idxs @@ -176,29 +191,30 @@ def parse_field(mongo_path, path, field): class DocumentImplementation( EmbeddedDocumentImplementation, - metaclass=MetaDocumentImplementation + metaclass=MetaDocumentImplementation, ): - """ - Represent a document once it has been implemented inside a + """Represent a document once it has been implemented inside a :class:`umongo.instance.BaseInstance`. .. note:: This class should not be used directly, it should be inherited by - concrete implementations such as :class:`umongo.frameworks.pymongo.PyMongoDocument` + concrete implementations such as + :class:`umongo.frameworks.pymongo.PyMongoDocument` """ - __slots__ = ('is_created', '_data') + __slots__ = ("_data", "is_created") opts = DocumentOpts(None, DocumentTemplate, abstract=True) def __init__(self, **kwargs): if self.opts.abstract: raise AbstractDocumentError("Cannot instantiate an abstract Document") self.is_created = False - "Return True if the document has been commited to database" # is_created's docstring super().__init__(**kwargs) def __repr__(self): - return '' % ( - self.__module__, self.__class__.__name__, dict(self._data.items())) + return ( + f"" + ) def __eq__(self, other): if self.pk is None: @@ -219,23 +235,20 @@ def clone(self): new = self.__class__() data = deepcopy(self._data._data) # Replace ID with new ID ("missing" unless a default value is provided) - data['_id'] = new._data._data['_id'] + data["_id"] = new._data._data["_id"] new._data._data = data new._data._modified_data = set(data.keys()) return new @property def collection(self): - """ - Return the collection used by this document class - """ + """Return the collection used by this document class""" # Cannot implicitly access to the class's property return type(self).collection @property def pk(self): - """ - Return the document's primary key (i.e. ``_id`` in mongo notation) or + """Return the document's primary key (i.e. ``_id`` in mongo notation) or None if not available yet .. warning:: Use ``is_created`` field instead to test if the document @@ -247,33 +260,30 @@ def pk(self): @property def dbref(self): - """ - Return a pymongo DBRef instance related to the document - """ + """Return a pymongo DBRef instance related to the document""" if not self.is_created: - raise NotCreatedError('Must create the document before' - ' having access to DBRef') + raise NotCreatedError( + "Must create the document before having access to DBRef", + ) return DBRef(collection=self.collection.name, id=self.pk) @classmethod def build_from_mongo(cls, data, use_cls=False): - """ - Create a document instance from MongoDB data + """Create a document instance from MongoDB data :param data: data as retrieved from MongoDB :param use_cls: if the data contains a ``_cls`` field, use it determine the Document class to instanciate """ # If a _cls is specified, we have to use this document class - if use_cls and '_cls' in data: - cls = cls.opts.instance.retrieve_document(data['_cls']) + if use_cls and "_cls" in data: + cls = cls.opts.instance.retrieve_document(data["_cls"]) doc = cls() doc.from_mongo(data) return doc def from_mongo(self, data): - """ - Update the document with the MongoDB data + """Update the document with the MongoDB data :param data: data as retrieved from MongoDB """ @@ -281,32 +291,27 @@ def from_mongo(self, data): self.is_created = True def to_mongo(self, update=False): - """ - Return the document as a dict compatible with MongoDB driver. + """Return the document as a dict compatible with MongoDB driver. :param update: if True the return dict should be used as an update payload instead of containing the entire document """ if update and not self.is_created: - raise NotCreatedError('Must create the document before using update') + raise NotCreatedError("Must create the document before using update") return self._data.to_mongo(update=update) def update(self, data): """Update the document with the given data.""" - if self.is_created and self.pk_field in data.keys(): + if self.is_created and self.pk_field in data: raise AlreadyCreatedError("Can't modify id of a created document") self._data.update(data) def dump(self): - """ - Dump the document. - """ + """Dump the document.""" return self._data.dump() def is_modified(self): - """ - Returns True if and only if the document was modified since last commit. - """ + """Returns True if and only if the document was modified since last commit.""" return not self.is_created or self._data.is_modified() # Data-proxy accessor shortcuts @@ -340,15 +345,13 @@ def __delattr__(self, name): # Callbacks def pre_insert(self): - """ - Overload this method to get a callback before document insertion. + """Overload this method to get a callback before document insertion. .. note:: If you use an async driver, this callback can be asynchronous. """ def pre_update(self): - """ - Overload this method to get a callback before document update. + """Overload this method to get a callback before document update. :return: Additional filters dict that will be used for the query to select the document to update. @@ -356,8 +359,7 @@ def pre_update(self): """ def pre_delete(self): - """ - Overload this method to get a callback before document deletion. + """Overload this method to get a callback before document deletion. :return: Additional filters dict that will be used for the query to select the document to update. @@ -365,24 +367,21 @@ def pre_delete(self): """ def post_insert(self, ret): - """ - Overload this method to get a callback after document insertion. + """Overload this method to get a callback after document insertion. :param ret: Pymongo response sent by the database. .. note:: If you use an async driver, this callback can be asynchronous. """ def post_update(self, ret): - """ - Overload this method to get a callback after document update. + """Overload this method to get a callback after document update. :param ret: Pymongo response sent by the database. .. note:: If you use an async driver, this callback can be asynchronous. """ def post_delete(self, ret): - """ - Overload this method to get a callback after document deletion. + """Overload this method to get a callback after document deletion. :param ret: Pymongo response sent by the database. .. note:: If you use an async driver, this callback can be asynchronous. diff --git a/umongo/embedded_document.py b/src/umongo/embedded_document.py similarity index 73% rename from umongo/embedded_document.py rename to src/umongo/embedded_document.py index 25b92b54..5a5dff9d 100644 --- a/umongo/embedded_document.py +++ b/src/umongo/embedded_document.py @@ -1,23 +1,22 @@ """umongo EmbeddedDocument""" + import marshmallow as ma -from .template import Implementation, Template from .data_objects import BaseDataObject -from .expose_missing import EXPOSE_MISSING from .exceptions import AbstractDocumentError - +from .expose_missing import EXPOSE_MISSING +from .template import Implementation, Template __all__ = ( - 'EmbeddedDocumentTemplate', - 'EmbeddedDocument', - 'EmbeddedDocumentOpts', - 'EmbeddedDocumentImplementation' + "EmbeddedDocument", + "EmbeddedDocumentImplementation", + "EmbeddedDocumentOpts", + "EmbeddedDocumentTemplate", ) class EmbeddedDocumentTemplate(Template): - """ - Base class to define a umongo embedded document. + """Base class to define a umongo embedded document. .. note:: Once defined, this class must be registered inside a @@ -31,8 +30,7 @@ class EmbeddedDocumentTemplate(Template): class EmbeddedDocumentOpts: - """ - Configuration for an :class:`umongo.embedded_document.EmbeddedDocument`. + """Configuration for an :class:`umongo.embedded_document.EmbeddedDocument`. Should be passed as a Meta class to the :class:`EmbeddedDocument` @@ -43,6 +41,7 @@ class MyEmbeddedDoc(EmbeddedDocument): class Meta: abstract = True + assert MyEmbeddedDoc.opts.abstract == True @@ -52,25 +51,35 @@ class Meta: template no Origin template of the embedded document instance no Implementation's instance abstract yes Embedded document can only be inherited - is_child no Embedded document inherit of a non-abstract - embedded document + is_child no Embedded document inherit of a + non-abstract embedded document strict yes Don't accept unknown fields from mongo (default: True) - offspring no List of embedded documents inheriting this one + offspring no List of embedded documents inheriting + this one ==================== ====================== =========== """ + def __repr__(self): - return ('<{ClassName}(' - 'instance={self.instance}, ' - 'template={self.template}, ' - 'abstract={self.abstract}, ' - 'is_child={self.is_child}, ' - 'strict={self.strict}, ' - 'offspring={self.offspring})>' - .format(ClassName=self.__class__.__name__, self=self)) - - def __init__(self, instance, template, abstract=False, - is_child=False, strict=True, offspring=None): + return ( + f"<{self.__class__.__name__}(" + f"instance={self.instance}, " + f"template={self.template}, " + f"abstract={self.abstract}, " + f"is_child={self.is_child}, " + f"strict={self.strict}, " + f"offspring={self.offspring})>" + ) + + def __init__( + self, + instance, + template, + abstract=False, + is_child=False, + strict=True, + offspring=None, + ): self.instance = instance self.template = template self.abstract = abstract @@ -80,28 +89,31 @@ def __init__(self, instance, template, abstract=False, class EmbeddedDocumentImplementation(Implementation, BaseDataObject): - """ - Represent an embedded document once it has been implemented inside a + """Represent an embedded document once it has been implemented inside a :class:`umongo.instance.BaseInstance`. """ - __slots__ = ('_data', ) + __slots__ = ("_data",) opts = EmbeddedDocumentOpts(None, EmbeddedDocumentTemplate, abstract=True) def __init__(self, **kwargs): super().__init__() if self.opts.abstract: - raise AbstractDocumentError("Cannot instantiate an abstract EmbeddedDocument") + raise AbstractDocumentError( + "Cannot instantiate an abstract EmbeddedDocument", + ) self._data = self.DataProxy(kwargs) def __repr__(self): - return '' % ( - self.__module__, self.__class__.__name__, dict(self._data.items())) + return ( + f"" + ) def __eq__(self, other): if isinstance(other, dict): return self._data == other - if hasattr(other, '_data'): + if hasattr(other, "_data"): return self._data == other._data return NotImplemented @@ -109,9 +121,7 @@ def is_modified(self): return self._data.is_modified() def clear_modified(self): - """ - Reset the list of document's modified items. - """ + """Reset the list of document's modified items.""" self._data.clear_modified() def required_validate(self): @@ -119,16 +129,15 @@ def required_validate(self): @classmethod def build_from_mongo(cls, data, use_cls=True): - """ - Create an embedded document instance from MongoDB data + """Create an embedded document instance from MongoDB data :param data: data as retrieved from MongoDB :param use_cls: if the data contains a ``_cls`` field, use it determine the EmbeddedDocument class to instanciate """ # If a _cls is specified, we have to use this document class - if use_cls and '_cls' in data: - cls = cls.opts.instance.retrieve_embedded_document(data['_cls']) + if use_cls and "_cls" in data: + cls = cls.opts.instance.retrieve_embedded_document(data["_cls"]) doc = cls() doc.from_mongo(data) return doc @@ -140,15 +149,11 @@ def to_mongo(self, update=False): return self._data.to_mongo(update=update) def update(self, data): - """ - Update the embedded document with the given data. - """ + """Update the embedded document with the given data.""" return self._data.update(data) def dump(self): - """ - Dump the embedded document. - """ + """Dump the embedded document.""" return self._data.dump() def items(self): diff --git a/umongo/exceptions.py b/src/umongo/exceptions.py similarity index 100% rename from umongo/exceptions.py rename to src/umongo/exceptions.py diff --git a/umongo/expose_missing.py b/src/umongo/expose_missing.py similarity index 89% rename from umongo/expose_missing.py rename to src/umongo/expose_missing.py index a9d35353..f6fbc359 100644 --- a/umongo/expose_missing.py +++ b/src/umongo/expose_missing.py @@ -3,15 +3,15 @@ Allows the user to let umongo document return missing rather than None for empty fields. """ -from contextvars import ContextVar + from contextlib import AbstractContextManager +from contextvars import ContextVar import marshmallow as ma - __all__ = ( - 'ExposeMissing', - 'RemoveMissingSchema', + "ExposeMissing", + "RemoveMissingSchema", ) @@ -26,6 +26,7 @@ class ExposeMissing(AbstractContextManager): be useful is cases where the user want to distinguish between None and missing value. """ + def __enter__(self): self.token = EXPOSE_MISSING.set(True) @@ -34,11 +35,11 @@ def __exit__(self, *args, **kwargs): class RemoveMissingSchema(ma.Schema): - """ - Custom :class:`marshmallow.Schema` subclass that skips missing fields + """Custom :class:`marshmallow.Schema` subclass that skips missing fields rather than returning None for missing fields when dumping umongo :class:`umongo.Document`s. """ + def dump(self, *args, **kwargs): with ExposeMissing(): return super().dump(*args, **kwargs) diff --git a/umongo/fields.py b/src/umongo/fields.py similarity index 74% rename from umongo/fields.py rename to src/umongo/fields.py index 013262ec..950dd9f7 100644 --- a/umongo/fields.py +++ b/src/umongo/fields.py @@ -1,51 +1,45 @@ """umongo fields""" + import collections import datetime as dt -from bson import DBRef, ObjectId, Decimal128 import marshmallow as ma -# from .registerer import retrieve_document -from .document import DocumentImplementation -from .exceptions import NotRegisteredDocumentError, DocumentDefinitionError -from .template import get_template -from .data_objects import Reference, List, Dict +from bson import DBRef, Decimal128, ObjectId + from . import marshmallow_bonus as ma_bonus_fields from .abstract import BaseField, I18nErrorDict +from .data_objects import Dict, List, Reference +from .document import DocumentImplementation +from .exceptions import DocumentDefinitionError, NotRegisteredDocumentError from .i18n import gettext as _ - +from .template import get_template __all__ = ( - # 'RawField', - # 'MappingField', - # 'TupleField', - 'StringField', - 'UUIDField', - 'NumberField', - 'IntegerField', - 'DecimalField', - 'BooleanField', - 'FloatField', - 'DateTimeField', - 'NaiveDateTimeField', - 'AwareDateTimeField', - # 'TimeField', - 'DateField', - # 'TimeDeltaField', - 'UrlField', - 'URLField', - 'EmailField', - 'StrField', - 'BoolField', - 'IntField', - 'DictField', - 'ListField', - 'ConstantField', - # 'PluckField' - 'ObjectIdField', - 'ReferenceField', - 'GenericReferenceField', - 'EmbeddedField' + "AwareDateTimeField", + "BoolField", + "BooleanField", + "ConstantField", + "DateField", + "DateTimeField", + "DecimalField", + "DictField", + "EmailField", + "EmbeddedField", + "FloatField", + "GenericReferenceField", + "IntField", + "IntegerField", + "ListField", + "NaiveDateTimeField", + "NumberField", + "ObjectIdField", + "ReferenceField", + "StrField", + "StringField", + "URLField", + "UUIDField", + "UrlField", ) @@ -101,7 +95,6 @@ def _round_to_millisecond(datetime): class DateTimeField(BaseField, ma.fields.DateTime): - def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, dt.datetime): ret = value @@ -111,7 +104,6 @@ def _deserialize(self, value, attr, data, **kwargs): class NaiveDateTimeField(BaseField, ma.fields.NaiveDateTime): - def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, dt.datetime): ret = value @@ -121,7 +113,6 @@ def _deserialize(self, value, attr, data, **kwargs): class AwareDateTimeField(BaseField, ma.fields.AwareDateTime): - def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, dt.datetime): ret = value @@ -172,7 +163,6 @@ class ConstantField(BaseField, ma.fields.Constant): class DictField(BaseField, ma.fields.Dict): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -183,10 +173,16 @@ def cast_value_or_callable(key_field, value_field, value): return lambda: Dict(key_field, value_field, value()) return Dict(key_field, value_field, value) - if hasattr(self, 'default'): - self.default = cast_value_or_callable(self.key_field, self.value_field, self.default) - if hasattr(self, 'missing'): - self.missing = cast_value_or_callable(self.key_field, self.value_field, self.missing) + self.dump_default = cast_value_or_callable( + self.key_field, + self.value_field, + self.dump_default, + ) + self.load_default = cast_value_or_callable( + self.key_field, + self.value_field, + self.load_default, + ) def _deserialize(self, value, attr, data, **kwargs): value = super()._deserialize(value, attr, data, **kwargs) @@ -196,8 +192,9 @@ def _serialize_to_mongo(self, obj): if obj is None: return ma.missing return { - self.key_field.serialize_to_mongo(k) if self.key_field else k: - self.value_field.serialize_to_mongo(v) if self.value_field else v + self.key_field.serialize_to_mongo(k) + if self.key_field + else k: self.value_field.serialize_to_mongo(v) if self.value_field else v for k, v in obj.items() } @@ -207,10 +204,13 @@ def _deserialize_from_mongo(self, value): self.key_field, self.value_field, { - self.key_field.deserialize_from_mongo(k) if self.key_field else k: - self.value_field.deserialize_from_mongo(v) if self.value_field else v + self.key_field.deserialize_from_mongo(k) + if self.key_field + else k: self.value_field.deserialize_from_mongo(v) + if self.value_field + else v for k, v in value.items() - } + }, ) return Dict(self.key_field, self.value_field) @@ -221,12 +221,16 @@ def as_marshmallow_field(self): else: inner_ma_field = None m_field = ma.fields.Dict( - self.key_field, inner_ma_field, metadata=self.metadata, **field_kwargs) + self.key_field, + inner_ma_field, + metadata=self.metadata, + **field_kwargs, + ) m_field.error_messages = I18nErrorDict(m_field.error_messages) return m_field def _required_validate(self, value): - if not hasattr(self.value_field, '_required_validate'): + if not hasattr(self.value_field, "_required_validate"): return required_validate = self.value_field._required_validate errors = collections.defaultdict(dict) @@ -240,7 +244,6 @@ def _required_validate(self, value): class ListField(BaseField, ma.fields.List): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -251,10 +254,8 @@ def cast_value_or_callable(inner, value): return lambda: List(inner, value()) return List(inner, value) - if hasattr(self, 'default'): - self.default = cast_value_or_callable(self.inner, self.default) - if hasattr(self, 'missing'): - self.missing = cast_value_or_callable(self.inner, self.missing) + self.dump_default = cast_value_or_callable(self.inner, self.dump_default) + self.load_default = cast_value_or_callable(self.inner, self.load_default) def _deserialize(self, value, attr, data, **kwargs): value = super()._deserialize(value, attr, data, **kwargs) @@ -269,15 +270,14 @@ def _deserialize_from_mongo(self, value): if value: return List( self.inner, - [self.inner.deserialize_from_mongo(each) for each in value] + [self.inner.deserialize_from_mongo(each) for each in value], ) return List(self.inner) def map_to_field(self, mongo_path, path, func): - """Apply a function to every field in the schema - """ + """Apply a function to every field in the schema""" func(mongo_path, path, self.inner) - if hasattr(self.inner, 'map_to_field'): + if hasattr(self.inner, "map_to_field"): self.inner.map_to_field(mongo_path, path, func) def as_marshmallow_field(self): @@ -288,7 +288,7 @@ def as_marshmallow_field(self): return m_field def _required_validate(self, value): - if not hasattr(self.inner, '_required_validate'): + if not hasattr(self.inner, "_required_validate"): return required_validate = self.inner._required_validate errors = {} @@ -313,12 +313,10 @@ class ObjectIdField(BaseField, ma_bonus_fields.ObjectId): class ReferenceField(BaseField, ma_bonus_fields.Reference): - def __init__(self, document, *args, reference_cls=Reference, **kwargs): - """ - :param document: Can be a :class:`umongo.embedded_document.DocumentTemplate`, - another instance's :class:`umongo.embedded_document.DocumentImplementation` or - the embedded document class name. + """:param document: Can be a :class:`umongo.embedded_document.DocumentTemplate`, + another instance's :class:`umongo.embedded_document.DocumentImplementation` + or the embedded document class name. .. warning:: The referenced document's _id must be an `ObjectId`. """ @@ -335,8 +333,8 @@ def __init__(self, document, *args, reference_cls=Reference, **kwargs): @property def document_cls(self): - """ - Return the instance's :class:`umongo.embedded_document.DocumentImplementation` + """Return the instance's + :class:`umongo.embedded_document.DocumentImplementation` implementing the `document` attribute. """ if not self._document_cls: @@ -348,24 +346,34 @@ def _deserialize(self, value, attr, data, **kwargs): return None if isinstance(value, DBRef): if self.document_cls.collection.name != value.collection: - raise ma.ValidationError(_("DBRef must be on collection `{collection}`.").format( - self.document_cls.collection.name)) + raise ma.ValidationError( + _("DBRef must be on collection `{collection}`.").format( + self.document_cls.collection.name, + ), + ) value = value.id elif isinstance(value, Reference): if value.document_cls != self.document_cls: - raise ma.ValidationError(_("`{document}` reference expected.").format( - document=self.document_cls.__name__)) + raise ma.ValidationError( + _("`{document}` reference expected.").format( + document=self.document_cls.__name__, + ), + ) if not isinstance(value, self.reference_cls): value = self.reference_cls(value.document_cls, value.pk) return value elif isinstance(value, self.document_cls): if not value.is_created: raise ma.ValidationError( - _("Cannot reference a document that has not been created yet.")) + _("Cannot reference a document that has not been created yet."), + ) value = value.pk elif isinstance(value, self._document_implementation_cls): - raise ma.ValidationError(_("`{document}` reference expected.").format( - document=self.document_cls.__name__)) + raise ma.ValidationError( + _("`{document}` reference expected.").format( + document=self.document_cls.__name__, + ), + ) value = super()._deserialize(value, attr, data, **kwargs) return self.reference_cls(self.document_cls, value) @@ -377,7 +385,6 @@ def _deserialize_from_mongo(self, value): class GenericReferenceField(BaseField, ma_bonus_fields.GenericReference): - def __init__(self, *args, reference_cls=Reference, **kwargs): super().__init__(*args, **kwargs) self.reference_cls = reference_cls @@ -386,14 +393,15 @@ def __init__(self, *args, reference_cls=Reference, **kwargs): def _document_cls(self, class_name): try: return self.instance.retrieve_document(class_name) - except NotRegisteredDocumentError: - raise ma.ValidationError(_('Unknown document `{document}`.').format( - document=class_name)) + except NotRegisteredDocumentError as exc: + raise ma.ValidationError( + _("Unknown document `{document}`.").format(document=class_name), + ) from exc def _serialize(self, value, attr, obj): if value is None: return None - return {'id': str(value.pk), 'cls': value.document_cls.__name__} + return {"id": str(value.pk), "cls": value.document_cls.__name__} def _deserialize(self, value, attr, data, **kwargs): if value is None: @@ -405,35 +413,37 @@ def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, self._document_implementation_cls): if not value.is_created: raise ma.ValidationError( - _("Cannot reference a document that has not been created yet.")) + _("Cannot reference a document that has not been created yet."), + ) return self.reference_cls(value.__class__, value.pk) if isinstance(value, dict): - if value.keys() != {'cls', 'id'}: - raise ma.ValidationError(_("Generic reference must have `id` and `cls` fields.")) + if value.keys() != {"cls", "id"}: + raise ma.ValidationError( + _("Generic reference must have `id` and `cls` fields."), + ) try: - _id = ObjectId(value['id']) - except ValueError: - raise ma.ValidationError(_("Invalid `id` field.")) - document_cls = self._document_cls(value['cls']) + _id = ObjectId(value["id"]) + except ValueError as exc: + raise ma.ValidationError(_("Invalid `id` field.")) from exc + document_cls = self._document_cls(value["cls"]) return self.reference_cls(document_cls, _id) raise ma.ValidationError(_("Invalid value for generic reference field.")) def _serialize_to_mongo(self, obj): - return {'_id': obj.pk, '_cls': obj.document_cls.__name__} + return {"_id": obj.pk, "_cls": obj.document_cls.__name__} def _deserialize_from_mongo(self, value): - document_cls = self._document_cls(value['_cls']) - return self.reference_cls(document_cls, value['_id']) + document_cls = self._document_cls(value["_cls"]) + return self.reference_cls(document_cls, value["_id"]) class EmbeddedField(BaseField, ma.fields.Nested): - def __init__(self, embedded_document, *args, **kwargs): - """ - :param embedded_document: Can be a - :class:`umongo.embedded_document.EmbeddedDocumentTemplate`, - another instance's :class:`umongo.embedded_document.EmbeddedDocumentImplementation` - or the embedded document class name. + """:param embedded_document: Can be a + :class:`umongo.embedded_document.EmbeddedDocumentTemplate`, + another instance's + :class:`umongo.embedded_document.EmbeddedDocumentImplementation` + or the embedded document class name. """ # Don't need to pass `nested` attribute given it is overloaded super().__init__(None, *args, **kwargs) @@ -455,15 +465,17 @@ def nested(self, value): @property def embedded_document_cls(self): - """ - Return the instance's :class:`umongo.embedded_document.EmbeddedDocumentImplementation` + """Return the instance's + :class:`umongo.embedded_document.EmbeddedDocumentImplementation` implementing the `embedded_document` attribute. """ if not self._embedded_document_cls: - embedded_document_cls = self.instance.retrieve_embedded_document(self.embedded_document) + embedded_document_cls = self.instance.retrieve_embedded_document( + self.embedded_document, + ) if embedded_document_cls.opts.abstract: raise DocumentDefinitionError( - "EmbeddedField doesn't accept abstract embedded document" + "EmbeddedField doesn't accept abstract embedded document", ) self._embedded_document_cls = embedded_document_cls return self._embedded_document_cls @@ -478,19 +490,28 @@ def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, embedded_document_cls): return value if not isinstance(value, dict): - raise ma.ValidationError({'_schema': ['Invalid input type.']}) + raise ma.ValidationError({"_schema": ["Invalid input type."]}) # Handle inheritance deserialization here using `cls` field as hint - if embedded_document_cls.opts.offspring and 'cls' in value: - to_use_cls_name = value.pop('cls') - if not any(o for o in embedded_document_cls.opts.offspring - if o.__name__ == to_use_cls_name): - raise ma.ValidationError(_('Unknown document `{document}`.').format( - document=to_use_cls_name)) + if embedded_document_cls.opts.offspring and "cls" in value: + to_use_cls_name = value.pop("cls") + if not any( + o + for o in embedded_document_cls.opts.offspring + if o.__name__ == to_use_cls_name + ): + raise ma.ValidationError( + _("Unknown document `{document}`.").format( + document=to_use_cls_name, + ), + ) try: - to_use_cls = embedded_document_cls.opts.instance.retrieve_embedded_document( - to_use_cls_name) + to_use_cls = ( + embedded_document_cls.opts.instance.retrieve_embedded_document( + to_use_cls_name, + ) + ) except NotRegisteredDocumentError as exc: - raise ma.ValidationError(str(exc)) + raise ma.ValidationError(str(exc)) from exc return to_use_cls(**value) return embedded_document_cls(**value) @@ -505,6 +526,7 @@ def _validate_missing(self, value): super()._validate_missing(value) errors = {} if value is ma.missing: + def get_sub_value(_): return ma.missing elif isinstance(value, dict): @@ -535,17 +557,21 @@ def get_sub_value(key): def map_to_field(self, mongo_path, path, func): """Apply a function to every field in the schema""" for name, field in self.embedded_document_cls.schema.fields.items(): - cur_path = '%s.%s' % (path, name) - cur_mongo_path = '%s.%s' % (mongo_path, field.attribute or name) + cur_path = f"{path}.{name}" + cur_mongo_path = f"{mongo_path}.{field.attribute or name}" func(cur_mongo_path, cur_path, field) - if hasattr(field, 'map_to_field'): + if hasattr(field, "map_to_field"): field.map_to_field(cur_mongo_path, cur_path, func) def as_marshmallow_field(self): # Overwrite default `as_marshmallow_field` to handle nesting field_kwargs = self._extract_marshmallow_field_params() nested_ma_schema = self.embedded_document_cls.schema.as_marshmallow_schema() - m_field = ma.fields.Nested(nested_ma_schema, metadata=self.metadata, **field_kwargs) + m_field = ma.fields.Nested( + nested_ma_schema, + metadata=self.metadata, + **field_kwargs, + ) m_field.error_messages = I18nErrorDict(m_field.error_messages) return m_field diff --git a/umongo/frameworks/__init__.py b/src/umongo/frameworks/__init__.py similarity index 78% rename from umongo/frameworks/__init__.py rename to src/umongo/frameworks/__init__.py index 4b764173..8fb322ca 100644 --- a/umongo/frameworks/__init__.py +++ b/src/umongo/frameworks/__init__.py @@ -1,28 +1,25 @@ -""" -Frameworks +"""Frameworks ========== """ -from ..exceptions import NoCompatibleInstanceError -from .pymongo import PyMongoInstance +from umongo.exceptions import NoCompatibleInstanceError -__all__ = ( - 'InstanceRegisterer', - - 'default_instance_registerer', - 'register_instance', - 'unregister_instance', - 'find_instance_from_db', +from .pymongo import PyMongoInstance - 'PyMongoInstance', - 'TxMongoInstance', - 'MotorAsyncIOInstance', - 'MongoMockInstance' +__all__ = ( + "InstanceRegisterer", + "MongoMockInstance", + "MotorAsyncIOInstance", + "PyMongoInstance", + "TxMongoInstance", + "default_instance_registerer", + "find_instance_from_db", + "register_instance", + "unregister_instance", ) class InstanceRegisterer: - def __init__(self): self.instances = [] @@ -40,7 +37,8 @@ def find_from_db(self, db): if instance.is_compatible_with(db): return instance raise NoCompatibleInstanceError( - 'Cannot find a umongo instance compatible with %s' % type(db)) + f"Cannot find a umongo instance compatible with {type(db)}", + ) default_instance_registerer = InstanceRegisterer() @@ -56,6 +54,7 @@ def find_from_db(self, db): try: from .txmongo import TxMongoInstance + register_instance(TxMongoInstance) except ImportError: # pragma: no cover pass @@ -63,6 +62,7 @@ def find_from_db(self, db): try: from .motor_asyncio import MotorAsyncIOInstance + register_instance(MotorAsyncIOInstance) except ImportError: # pragma: no cover pass @@ -70,6 +70,7 @@ def find_from_db(self, db): try: from .mongomock import MongoMockInstance + register_instance(MongoMockInstance) except ImportError: # pragma: no cover pass diff --git a/umongo/frameworks/mongomock.py b/src/umongo/frameworks/mongomock.py similarity index 71% rename from umongo/frameworks/mongomock.py rename to src/umongo/frameworks/mongomock.py index f17c2a82..acb0e03d 100644 --- a/umongo/frameworks/mongomock.py +++ b/src/umongo/frameworks/mongomock.py @@ -1,10 +1,10 @@ -from mongomock.database import Database from mongomock.collection import Cursor +from mongomock.database import Database -from .pymongo import PyMongoBuilder, PyMongoDocument, BaseWrappedCursor -from ..instance import Instance -from ..document import DocumentImplementation +from umongo.document import DocumentImplementation +from umongo.instance import Instance +from .pymongo import BaseWrappedCursor, PyMongoBuilder, PyMongoDocument # Mongomock aims at working like pymongo @@ -24,9 +24,8 @@ class MongoMockBuilder(PyMongoBuilder): class MongoMockInstance(Instance): - """ - :class:`umongo.instance.Instance` implementation for mongomock - """ + """:class:`umongo.instance.Instance` implementation for mongomock""" + BUILDER_CLS = MongoMockBuilder @staticmethod diff --git a/umongo/frameworks/motor_asyncio.py b/src/umongo/frameworks/motor_asyncio.py similarity index 74% rename from umongo/frameworks/motor_asyncio.py rename to src/umongo/frameworks/motor_asyncio.py index c944c14f..69c5f147 100644 --- a/umongo/frameworks/motor_asyncio.py +++ b/src/umongo/frameworks/motor_asyncio.py @@ -1,31 +1,41 @@ +import asyncio import collections -from contextvars import ContextVar +import types from contextlib import asynccontextmanager +from contextvars import ContextVar +from inspect import isawaitable -from inspect import iscoroutine -import asyncio - -from motor.motor_asyncio import AsyncIOMotorDatabase, AsyncIOMotorCursor -from pymongo.errors import DuplicateKeyError import marshmallow as ma -from ..builder import BaseBuilder -from ..instance import Instance -from ..document import DocumentImplementation -from ..data_objects import Reference -from ..exceptions import NotCreatedError, UpdateError, DeleteError, NoneReferenceError -from ..fields import ReferenceField, ListField, DictField, EmbeddedField -from ..query_mapper import map_query - -from .tools import cook_find_filter, cook_find_projection, remove_cls_field_from_embedded_docs +from motor.motor_asyncio import AsyncIOMotorCursor, AsyncIOMotorDatabase +from pymongo.errors import DuplicateKeyError +from umongo.builder import BaseBuilder +from umongo.data_objects import Reference +from umongo.document import DocumentImplementation +from umongo.exceptions import ( + DeleteError, + NoneReferenceError, + NotCreatedError, + UpdateError, +) +from umongo.fields import DictField, EmbeddedField, ListField, ReferenceField +from umongo.instance import Instance +from umongo.query_mapper import map_query + +from .tools import ( + cook_find_filter, + cook_find_projection, + remove_cls_field_from_embedded_docs, +) SESSION = ContextVar("session", default=None) +if not hasattr(asyncio, "coroutine"): + asyncio.coroutine = types.coroutine class WrappedCursor(AsyncIOMotorCursor): - - __slots__ = ('raw_cursor', 'document_cls') + __slots__ = ("document_cls", "raw_cursor") def __init__(self, document_cls, cursor): # Such a cunning plan my lord ! @@ -58,6 +68,7 @@ def wrapped_callback(result, error): if not error and result is not None: result = self.document_cls.build_from_mongo(result, use_cls=True) return callback(result, error) + return self.raw_cursor.each(wrapped_callback) def to_list(self, length, callback=None): @@ -74,7 +85,6 @@ def on_raw_done(fut): class MotorAsyncIODocument(DocumentImplementation): - __slots__ = () opts = DocumentImplementation.opts @@ -84,43 +94,42 @@ class MotorAsyncIODocument(DocumentImplementation): async def __coroutined_pre_insert(self): ret = self.pre_insert() - if iscoroutine(ret): + if isawaitable(ret): ret = await ret return ret async def __coroutined_pre_update(self): ret = self.pre_update() - if iscoroutine(ret): + if isawaitable(ret): ret = await ret return ret async def __coroutined_pre_delete(self): ret = self.pre_delete() - if iscoroutine(ret): + if isawaitable(ret): ret = await ret return ret async def __coroutined_post_insert(self, ret): ret = self.post_insert(ret) - if iscoroutine(ret): + if isawaitable(ret): ret = await ret return ret async def __coroutined_post_update(self, ret): ret = self.post_update(ret) - if iscoroutine(ret): + if isawaitable(ret): ret = await ret return ret async def __coroutined_post_delete(self, ret): ret = self.post_delete(ret) - if iscoroutine(ret): + if isawaitable(ret): ret = await ret return ret async def reload(self): - """ - Retrieve and replace document's data by the ones in database. + """Retrieve and replace document's data by the ones in database. Raises :class:`umongo.exceptions.NotCreatedError` if the document doesn't exist in database. @@ -134,8 +143,7 @@ async def reload(self): self._data.from_mongo(ret) async def commit(self, io_validate_all=False, conditions=None, replace=False): - """ - Commit the document in database. + """Commit the document in database. If the document doesn't already exist it will be inserted, otherwise it will be updated. @@ -152,7 +160,7 @@ async def commit(self, io_validate_all=False, conditions=None, replace=False): if self.is_created: if self.is_modified() or replace: query = conditions or {} - query['_id'] = self.pk + query["_id"] = self.pk # pre_update can provide additional query filter and/or # modify the fields' values additional_filter = await self.__coroutined_pre_update() @@ -163,11 +171,17 @@ async def commit(self, io_validate_all=False, conditions=None, replace=False): if replace: payload = self._data.to_mongo(update=False) ret = await self.collection.replace_one( - query, payload, session=SESSION.get()) + query, + payload, + session=SESSION.get(), + ) else: payload = self._data.to_mongo(update=True) ret = await self.collection.update_one( - query, payload, session=SESSION.get()) + query, + payload, + session=SESSION.get(), + ) if ret.matched_count != 1: raise UpdateError(ret) await self.__coroutined_post_update(ret) @@ -175,7 +189,7 @@ async def commit(self, io_validate_all=False, conditions=None, replace=False): ret = None elif conditions: raise NotCreatedError( - 'Document must already exist in database to use `conditions`.' + "Document must already exist in database to use `conditions`.", ) else: await self.__coroutined_pre_insert() @@ -189,31 +203,30 @@ async def commit(self, io_validate_all=False, conditions=None, replace=False): await self.__coroutined_post_insert(ret) except DuplicateKeyError as exc: # Sort value to make testing easier for compound indexes - keys = sorted(exc.details['keyPattern'].keys()) + keys = sorted(exc.details["keyPattern"].keys()) try: fields = [self.schema.fields[k] for k in keys] except KeyError: - # A key in the index is unknwon from umongo - raise exc + # A key in the index is unknown from umongo + raise exc from None if len(keys) == 1: - msg = fields[0].error_messages['unique'] - raise ma.ValidationError({keys[0]: msg}) - raise ma.ValidationError({ - k: f.error_messages['unique_compound'].format(fields=keys) - for k, f in zip(keys, fields) - }) + msg = fields[0].error_messages["unique"] + raise ma.ValidationError({keys[0]: msg}) from exc + raise ma.ValidationError( + { + k: f.error_messages["unique_compound"].format(fields=keys) + for k, f in zip(keys, fields, strict=True) + }, + ) from exc self._data.clear_modified() return ret async def delete(self, conditions=None): - """ - Alias of :meth:`remove` to enforce default api. - """ + """Alias of :meth:`remove` to enforce default api.""" return await self.remove(conditions=conditions) async def remove(self, conditions=None): - """ - Remove the document from database. + """Remove the document from database. :param conditions: Only perform delete if matching record in db satisfies condition(s) (e.g. version number). @@ -229,7 +242,7 @@ async def remove(self, conditions=None): if not self.is_created: raise NotCreatedError("Document doesn't exists in database") query = conditions or {} - query['_id'] = self.pk + query["_id"] = self.pk # pre_delete can provide additional query filter additional_filter = await self.__coroutined_pre_delete() if additional_filter: @@ -242,8 +255,7 @@ async def remove(self, conditions=None): return ret async def io_validate(self, validate_all=False): - """ - Run the io_validators of the document's fields. + """Run the io_validators of the document's fields. :param validate_all: If False only run the io_validators of the fields that have been modified. @@ -251,64 +263,80 @@ async def io_validate(self, validate_all=False): if validate_all: return await _io_validate_data_proxy(self.schema, self._data) return await _io_validate_data_proxy( - self.schema, self._data, partial=self._data.get_modified_fields()) + self.schema, + self._data, + partial=self._data.get_modified_fields(), + ) @classmethod - async def find_one(cls, filter=None, projection=None, *args, **kwargs): - """ - Find a single document in database. - """ + async def find_one(cls, filter=None, projection=None, **kwargs): + """Find a single document in database.""" filter = cook_find_filter(cls, filter) if projection: projection = cook_find_projection(cls, projection) - ret = await cls.collection.find_one(filter, projection=projection, - session=SESSION.get(), *args, **kwargs) + ret = await cls.collection.find_one( + filter, + projection=projection, + session=SESSION.get(), + **kwargs, + ) if ret is not None: ret = cls.build_from_mongo(ret, use_cls=True) return ret @classmethod def find(cls, filter=None, *args, **kwargs): - """ - Find a list document in database. + """Find a list document in database. Returns a cursor that provide Documents. """ filter = cook_find_filter(cls, filter) return WrappedCursor( cls, - cls.collection.find(filter, session=SESSION.get(), *args, **kwargs) + cls.collection.find(filter, *args, session=SESSION.get(), **kwargs), ) @classmethod async def count_documents(cls, filter=None, *, with_limit_and_skip=False, **kwargs): - """ - Return a count of the documents in a collection. - """ + """Return a count of the documents in a collection.""" filter = cook_find_filter(cls, filter or {}) - return await cls.collection.count_documents(filter, session=SESSION.get(), **kwargs) + return await cls.collection.count_documents( + filter, + session=SESSION.get(), + **kwargs, + ) @classmethod async def ensure_indexes(cls): - """ - Check&create if needed the Document's indexes in database - """ + """Check&create if needed the Document's indexes in database""" for index in cls.indexes: kwargs = index.document.copy() - keys = kwargs.pop('key').items() + keys = kwargs.pop("key").items() await cls.collection.create_index(keys, session=SESSION.get(), **kwargs) # Run multiple validators and collect all errors in one async def _run_validators(validators, field, value): errors = [] - tasks = [validator(field, value) for validator in validators] - results = await asyncio.gather(*tasks, return_exceptions=True) - for i, res in enumerate(results): - if isinstance(res, ma.ValidationError): - errors.extend(res.messages) - elif res: - raise res + tasks = [] + + for validator in validators: + try: + result = validator(field, value) + if isawaitable(result): + tasks.append(result) + elif result: # truthy -> treat as error + raise result + except ma.ValidationError as exc: + errors.extend(exc.messages) + + if tasks: + results = await asyncio.gather(*tasks, return_exceptions=True) + for res in results: + if isinstance(res, ma.ValidationError): + errors.extend(res.messages) + elif isinstance(res, Exception): + raise res if errors: raise ma.ValidationError(errors) @@ -346,8 +374,11 @@ async def _reference_io_validate(field, value): return exists = await value.exists if not exists: - raise ma.ValidationError(value.error_messages['not_found'].format( - document=value.document_cls.__name__)) + raise ma.ValidationError( + value.error_messages["not_found"].format( + document=value.document_cls.__name__, + ), + ) async def _list_io_validate(field, value): @@ -374,12 +405,12 @@ async def _dict_io_validate(field, value): validators = field.value_field.io_validate if not validators: return - tasks = [] - for key, val in value.items(): - tasks.append(_run_validators(validators, field.value_field, val)) + tasks = [ + _run_validators(validators, field.value_field, val) for val in value.values() + ] results = await asyncio.gather(*tasks, return_exceptions=True) errors = collections.defaultdict(dict) - for key, res in zip(value.keys(), results): + for key, res in zip(value.keys(), results, strict=True): if isinstance(res, ma.ValidationError): errors[key]["value"] = res.messages elif res: @@ -395,7 +426,6 @@ async def _embedded_document_io_validate(field, value): class MotorAsyncIOReference(Reference): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._document = None @@ -403,21 +433,31 @@ def __init__(self, *args, **kwargs): async def fetch(self, no_data=False, force_reload=False, projection=None): if not self._document or force_reload: if self.pk is None: - raise NoneReferenceError('Cannot retrieve a None Reference') - self._document = await self.document_cls.find_one(self.pk, projection=projection) + raise NoneReferenceError("Cannot retrieve a None Reference") + self._document = await self.document_cls.find_one( + self.pk, + projection=projection, + ) if not self._document: - raise ma.ValidationError(self.error_messages['not_found'].format( - document=self.document_cls.__name__)) + raise ma.ValidationError( + self.error_messages["not_found"].format( + document=self.document_cls.__name__, + ), + ) return self._document @property async def exists(self): - return await self.document_cls.collection.find_one(self.pk, - projection={'_id': True}) is not None + return ( + await self.document_cls.collection.find_one( + self.pk, + projection={"_id": True}, + ) + is not None + ) class MotorAsyncIOBuilder(BaseBuilder): - BASE_DOCUMENT_CLS = MotorAsyncIODocument def _patch_field(self, field): @@ -427,12 +467,12 @@ def _patch_field(self, field): if not validators: field.io_validate = [] else: - if hasattr(validators, '__iter__'): + if hasattr(validators, "__iter__"): validators = list(validators) else: validators = [validators] field.io_validate = [ - v if asyncio.iscoroutinefunction(v) else asyncio.coroutine(v) + v if asyncio.iscoroutinefunction(v) else types.coroutine(v) for v in validators ] if isinstance(field, ListField): @@ -447,9 +487,8 @@ def _patch_field(self, field): class MotorAsyncIOInstance(Instance): - """ - :class:`umongo.instance.Instance` implementation for motor-asyncio - """ + """:class:`umongo.instance.Instance` implementation for motor-asyncio""" + BUILDER_CLS = MotorAsyncIOBuilder @staticmethod @@ -475,7 +514,8 @@ async def migrate_2_to_3(self): - EmbeddedDocument _cls field is only set if child of concrete embedded document """ concrete_not_children = [ - name for name, ed in self._embedded_lookup.items() + name + for name, ed in self._embedded_lookup.items() if not ed.opts.is_child and not ed.opts.abstract ] diff --git a/umongo/frameworks/pymongo.py b/src/umongo/frameworks/pymongo.py similarity index 76% rename from umongo/frameworks/pymongo.py rename to src/umongo/frameworks/pymongo.py index 6feb53db..826272e0 100644 --- a/umongo/frameworks/pymongo.py +++ b/src/umongo/frameworks/pymongo.py @@ -1,22 +1,31 @@ import collections -from contextvars import ContextVar from contextlib import contextmanager +from contextvars import ContextVar -from pymongo.database import Database -from pymongo.cursor import Cursor -from pymongo.errors import DuplicateKeyError import marshmallow as ma -from ..builder import BaseBuilder -from ..instance import Instance -from ..document import DocumentImplementation -from ..data_objects import Reference -from ..exceptions import NotCreatedError, UpdateError, DeleteError, NoneReferenceError -from ..fields import ReferenceField, ListField, DictField, EmbeddedField -from ..query_mapper import map_query - -from .tools import cook_find_filter, cook_find_projection, remove_cls_field_from_embedded_docs +from pymongo.cursor import Cursor +from pymongo.database import Database +from pymongo.errors import DuplicateKeyError +from umongo.builder import BaseBuilder +from umongo.data_objects import Reference +from umongo.document import DocumentImplementation +from umongo.exceptions import ( + DeleteError, + NoneReferenceError, + NotCreatedError, + UpdateError, +) +from umongo.fields import DictField, EmbeddedField, ListField, ReferenceField +from umongo.instance import Instance +from umongo.query_mapper import map_query + +from .tools import ( + cook_find_filter, + cook_find_projection, + remove_cls_field_from_embedded_docs, +) SESSION = ContextVar("session", default=None) @@ -24,8 +33,7 @@ # pymongo.Cursor defines __del__ method, hence mongomock's WrappedCursor should # not inherit from this class otherwise garbage collection will crash... class BaseWrappedCursor: - - __slots__ = ('raw_cursor', 'document_cls') + __slots__ = ("document_cls", "raw_cursor") def __init__(self, document_cls, cursor, *args, **kwargs): # Such a cunning plan my lord ! @@ -43,8 +51,9 @@ def __setattr__(self, name, value): def __getitem__(self, index): if isinstance(index, slice): elems = self.raw_cursor[index] - return (self.document_cls.build_from_mongo(elem, use_cls=True) - for elem in elems) + return ( + self.document_cls.build_from_mongo(elem, use_cls=True) for elem in elems + ) elem = self.raw_cursor[index] return self.document_cls.build_from_mongo(elem, use_cls=True) @@ -62,15 +71,13 @@ class WrappedCursor(BaseWrappedCursor, Cursor): class PyMongoDocument(DocumentImplementation): - __slots__ = () cursor_cls = WrappedCursor # Easier to customize this for mongomock this way opts = DocumentImplementation.opts def reload(self): - """ - Retrieve and replace document's data by the ones in database. + """Retrieve and replace document's data by the ones in database. Raises :class:`umongo.exceptions.NotCreatedError` if the document doesn't exist in database. @@ -84,8 +91,7 @@ def reload(self): self._data.from_mongo(ret) def commit(self, io_validate_all=False, conditions=None, replace=False): - """ - Commit the document in database. + """Commit the document in database. If the document doesn't already exist it will be inserted, otherwise it will be updated. @@ -102,7 +108,7 @@ def commit(self, io_validate_all=False, conditions=None, replace=False): if self.is_created: if self.is_modified() or replace: query = conditions or {} - query['_id'] = self.pk + query["_id"] = self.pk # pre_update can provide additional query filter and/or # modify the fields' values additional_filter = self.pre_update() @@ -112,10 +118,18 @@ def commit(self, io_validate_all=False, conditions=None, replace=False): self.io_validate(validate_all=io_validate_all) if replace: payload = self._data.to_mongo(update=False) - ret = self.collection.replace_one(query, payload, session=SESSION.get()) + ret = self.collection.replace_one( + query, + payload, + session=SESSION.get(), + ) else: payload = self._data.to_mongo(update=True) - ret = self.collection.update_one(query, payload, session=SESSION.get()) + ret = self.collection.update_one( + query, + payload, + session=SESSION.get(), + ) if ret.matched_count != 1: raise UpdateError(ret) self.post_update(ret) @@ -123,7 +137,7 @@ def commit(self, io_validate_all=False, conditions=None, replace=False): ret = None elif conditions: raise NotCreatedError( - 'Document must already exist in database to use `conditions`.' + "Document must already exist in database to use `conditions`.", ) else: self.pre_insert() @@ -137,25 +151,26 @@ def commit(self, io_validate_all=False, conditions=None, replace=False): self.post_insert(ret) except DuplicateKeyError as exc: # Sort value to make testing easier for compound indexes - keys = sorted(exc.details['keyPattern'].keys()) + keys = sorted(exc.details["keyPattern"].keys()) try: fields = [self.schema.fields[k] for k in keys] except KeyError: - # A key in the index is unknwon from umongo - raise exc + # A key in the index is unknown from umongo + raise exc from None if len(keys) == 1: - msg = fields[0].error_messages['unique'] - raise ma.ValidationError({keys[0]: msg}) - raise ma.ValidationError({ - k: f.error_messages['unique_compound'].format(fields=keys) - for k, f in zip(keys, fields) - }) + msg = fields[0].error_messages["unique"] + raise ma.ValidationError({keys[0]: msg}) from exc + raise ma.ValidationError( + { + k: f.error_messages["unique_compound"].format(fields=keys) + for k, f in zip(keys, fields, strict=True) + }, + ) from exc self._data.clear_modified() return ret def delete(self, conditions=None): - """ - Remove the document from database. + """Remove the document from database. :param conditions: Only perform delete if matching record in db satisfies condition(s) (e.g. version number). @@ -171,7 +186,7 @@ def delete(self, conditions=None): if not self.is_created: raise NotCreatedError("Document doesn't exists in database") query = conditions or {} - query['_id'] = self.pk + query["_id"] = self.pk # pre_delete can provide additional query filter additional_filter = self.pre_delete() if additional_filter: @@ -184,8 +199,7 @@ def delete(self, conditions=None): return ret def io_validate(self, validate_all=False): - """ - Run the io_validators of the document's fields. + """Run the io_validators of the document's fields. :param validate_all: If False only run the io_validators of the fields that have been modified. @@ -194,37 +208,40 @@ def io_validate(self, validate_all=False): _io_validate_data_proxy(self.schema, self._data) else: _io_validate_data_proxy( - self.schema, self._data, partial=self._data.get_modified_fields()) + self.schema, + self._data, + partial=self._data.get_modified_fields(), + ) @classmethod - def find_one(cls, filter=None, projection=None, *args, **kwargs): - """ - Find a single document in database. - """ + def find_one(cls, filter=None, projection=None, **kwargs): + """Find a single document in database.""" filter = cook_find_filter(cls, filter) if projection: projection = cook_find_projection(cls, projection) - ret = cls.collection.find_one(filter, projection=projection, - session=SESSION.get(), *args, **kwargs) + ret = cls.collection.find_one( + filter, + projection=projection, + session=SESSION.get(), + **kwargs, + ) if ret is not None: ret = cls.build_from_mongo(ret, use_cls=True) return ret @classmethod def find(cls, filter=None, *args, **kwargs): - """ - Find a list document in database. + """Find a list document in database. Returns a cursor that provide Documents. """ filter = cook_find_filter(cls, filter) - raw_cursor = cls.collection.find(filter, session=SESSION.get(), *args, **kwargs) + raw_cursor = cls.collection.find(filter, *args, session=SESSION.get(), **kwargs) return cls.cursor_cls(cls, raw_cursor) @classmethod def count_documents(cls, filter=None, **kwargs): - """ - Get the number of documents in this collection. + """Get the number of documents in this collection. Unlike pymongo's collection.count_documents, filter is optional and defaults to an empty filter. @@ -234,16 +251,14 @@ def count_documents(cls, filter=None, **kwargs): @classmethod def ensure_indexes(cls): - """ - Check&create if needed the Document's indexes in database - """ + """Check&create if needed the Document's indexes in database""" if cls.indexes: cls.collection.create_indexes(cls.indexes, session=SESSION.get()) # Run multiple validators and collect all errors in one def _run_validators(validators, field, value): - if not hasattr(validators, '__iter__'): + if not hasattr(validators, "__iter__"): validators(field, value) else: errors = [] @@ -279,8 +294,11 @@ def _reference_io_validate(field, value): if value is None: return if not value.exists: - raise ma.ValidationError(value.error_messages['not_found'].format( - document=value.document_cls.__name__)) + raise ma.ValidationError( + value.error_messages["not_found"].format( + document=value.document_cls.__name__, + ), + ) def _list_io_validate(field, value): @@ -322,7 +340,6 @@ def _embedded_document_io_validate(field, value): class PyMongoReference(Reference): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._document = None @@ -330,20 +347,25 @@ def __init__(self, *args, **kwargs): def fetch(self, no_data=False, force_reload=False, projection=None): if not self._document or force_reload: if self.pk is None: - raise NoneReferenceError('Cannot retrieve a None Reference') + raise NoneReferenceError("Cannot retrieve a None Reference") self._document = self.document_cls.find_one(self.pk, projection=projection) if not self._document: - raise ma.ValidationError(self.error_messages['not_found'].format( - document=self.document_cls.__name__)) + raise ma.ValidationError( + self.error_messages["not_found"].format( + document=self.document_cls.__name__, + ), + ) return self._document @property def exists(self): - return self.document_cls.collection.find_one(self.pk, projection={'_id': True}) is not None + return ( + self.document_cls.collection.find_one(self.pk, projection={"_id": True}) + is not None + ) class PyMongoBuilder(BaseBuilder): - BASE_DOCUMENT_CLS = PyMongoDocument def _patch_field(self, field): @@ -352,11 +374,10 @@ def _patch_field(self, field): validators = field.io_validate if not validators: field.io_validate = [] + elif hasattr(validators, "__iter__"): + field.io_validate = list(validators) else: - if hasattr(validators, '__iter__'): - field.io_validate = list(validators) - else: - field.io_validate = [validators] + field.io_validate = [validators] if isinstance(field, ListField): field.io_validate_recursive = _list_io_validate if isinstance(field, DictField): @@ -369,9 +390,8 @@ def _patch_field(self, field): class PyMongoInstance(Instance): - """ - :class:`umongo.instance.Instance` implementation for pymongo - """ + """:class:`umongo.instance.Instance` implementation for pymongo""" + BUILDER_CLS = PyMongoBuilder @staticmethod @@ -397,7 +417,8 @@ def migrate_2_to_3(self): - EmbeddedDocument _cls field is only set if child of concrete embedded document """ concrete_not_children = [ - name for name, ed in self._embedded_lookup.items() + name + for name, ed in self._embedded_lookup.items() if not ed.opts.is_child and not ed.opts.abstract ] diff --git a/umongo/frameworks/tools.py b/src/umongo/frameworks/tools.py similarity index 77% rename from umongo/frameworks/tools.py rename to src/umongo/frameworks/tools.py index 36176e2a..67a3935c 100644 --- a/umongo/frameworks/tools.py +++ b/src/umongo/frameworks/tools.py @@ -1,9 +1,8 @@ -from ..query_mapper import map_query +from umongo.query_mapper import map_query def cook_find_filter(doc_cls, filter): - """ - Add the `_cls` field if needed and replace the fields' name by the one + """Add the `_cls` field if needed and replace the fields' name by the one they have in database. """ filter = map_query(filter, doc_cls.schema.fields) @@ -11,30 +10,31 @@ def cook_find_filter(doc_cls, filter): filter = filter or {} # Filter should be either a dict or an id if not isinstance(filter, dict): - filter = {'_id': filter} + filter = {"_id": filter} # Current document shares the collection with a parent, # we must use the _cls field to discriminate if doc_cls.opts.offspring: # Current document has itself offspring, we also have # to search through them - filter['_cls'] = { - '$in': [o.__name__ for o in doc_cls.opts.offspring] + [doc_cls.__name__]} + filter["_cls"] = { + "$in": [o.__name__ for o in doc_cls.opts.offspring] + + [doc_cls.__name__], + } else: - filter['_cls'] = doc_cls.__name__ + filter["_cls"] = doc_cls.__name__ return filter def cook_find_projection(doc_cls, projection): - """ - Replace field names in a projection by their database names. - """ + """Replace field names in a projection by their database names.""" # a projection may be either: # - a list of field names to return, or - # - a dict of field names and values to either return (value of 1) or not return (value of 0) + # - a dict of field names and values to either return (value of 1) or + # not return (value of 0) # in order to reuse as much of the `cook_find_filter` logic as possible, # convert a list projection to a dict which produces the same result if isinstance(projection, list): - projection = {field: 1 for field in projection} + projection = dict.fromkeys(projection, 1) projection = map_query(projection, doc_cls.schema.fields) return projection @@ -57,7 +57,6 @@ def remove_cls_field_from_embedded_docs(dict_in, embedded_docs): } if isinstance(dict_in, list): return [ - remove_cls_field_from_embedded_docs(item, embedded_docs) - for item in dict_in + remove_cls_field_from_embedded_docs(item, embedded_docs) for item in dict_in ] return dict_in diff --git a/umongo/frameworks/txmongo.py b/src/umongo/frameworks/txmongo.py similarity index 80% rename from umongo/frameworks/txmongo.py rename to src/umongo/frameworks/txmongo.py index 5b440108..ada077d4 100644 --- a/umongo/frameworks/txmongo.py +++ b/src/umongo/frameworks/txmongo.py @@ -1,31 +1,44 @@ -from twisted.internet.defer import ( - inlineCallbacks, Deferred, DeferredList, returnValue, maybeDeferred) -from txmongo import filter as qf -from txmongo.database import Database -from pymongo.errors import DuplicateKeyError import marshmallow as ma -from ..builder import BaseBuilder -from ..instance import Instance -from ..document import DocumentImplementation -from ..data_objects import Reference -from ..exceptions import NotCreatedError, UpdateError, DeleteError, NoneReferenceError -from ..fields import ReferenceField, ListField, DictField, EmbeddedField -from ..query_mapper import map_query +from pymongo.errors import DuplicateKeyError +from txmongo import filter as qf +from txmongo.database import Database -from .tools import cook_find_filter, cook_find_projection, remove_cls_field_from_embedded_docs +from twisted.internet.defer import ( + Deferred, + DeferredList, + inlineCallbacks, + maybeDeferred, +) + +from umongo.builder import BaseBuilder +from umongo.data_objects import Reference +from umongo.document import DocumentImplementation +from umongo.exceptions import ( + DeleteError, + NoneReferenceError, + NotCreatedError, + UpdateError, +) +from umongo.fields import DictField, EmbeddedField, ListField, ReferenceField +from umongo.instance import Instance +from umongo.query_mapper import map_query + +from .tools import ( + cook_find_filter, + cook_find_projection, + remove_cls_field_from_embedded_docs, +) class TxMongoDocument(DocumentImplementation): - __slots__ = () opts = DocumentImplementation.opts @inlineCallbacks def reload(self): - """ - Retrieve and replace document's data by the ones in database. + """Retrieve and replace document's data by the ones in database. Raises :class:`umongo.exceptions.NotCreatedError` if the document doesn't exist in database. @@ -40,8 +53,7 @@ def reload(self): @inlineCallbacks def commit(self, io_validate_all=False, conditions=None, replace=False): - """ - Commit the document in database. + """Commit the document in database. If the document doesn't already exist it will be inserted, otherwise it will be updated. @@ -53,12 +65,12 @@ def commit(self, io_validate_all=False, conditions=None, replace=False): :param replace: Replace the document rather than update. :return: A :class:`pymongo.results.UpdateResult` or :class:`pymongo.results.InsertOneResult` depending of the operation. - """ + """ try: if self.is_created: if self.is_modified() or replace: query = conditions or {} - query['_id'] = self.pk + query["_id"] = self.pk # pre_update can provide additional query filter and/or # modify the fields' values additional_filter = yield maybeDeferred(self.pre_update) @@ -79,7 +91,7 @@ def commit(self, io_validate_all=False, conditions=None, replace=False): ret = None elif conditions: raise NotCreatedError( - 'Document must already exist in database to use `conditions`.' + "Document must already exist in database to use `conditions`.", ) else: yield maybeDeferred(self.pre_insert) @@ -93,26 +105,27 @@ def commit(self, io_validate_all=False, conditions=None, replace=False): yield maybeDeferred(self.post_insert, ret) except DuplicateKeyError as exc: # Sort value to make testing easier for compound indexes - keys = sorted(exc.details['keyPattern'].keys()) + keys = sorted(exc.details["keyPattern"].keys()) try: fields = [self.schema.fields[k] for k in keys] except KeyError: - # A key in the index is unknwon from umongo - raise exc + # A key in the index is unknown from umongo + raise exc from None if len(keys) == 1: - msg = fields[0].error_messages['unique'] - raise ma.ValidationError({keys[0]: msg}) - raise ma.ValidationError({ - k: f.error_messages['unique_compound'].format(fields=keys) - for k, f in zip(keys, fields) - }) + msg = fields[0].error_messages["unique"] + raise ma.ValidationError({keys[0]: msg}) from exc + raise ma.ValidationError( + { + k: f.error_messages["unique_compound"].format(fields=keys) + for k, f in zip(keys, fields, strict=True) + }, + ) from exc self._data.clear_modified() return ret @inlineCallbacks def delete(self, conditions=None): - """ - Remove the document from database. + """Remove the document from database. :param conditions: Only perform delete if matching record in db satisfies condition(s) (e.g. version number). @@ -128,7 +141,7 @@ def delete(self, conditions=None): if not self.is_created: raise NotCreatedError("Document doesn't exists in database") query = conditions or {} - query['_id'] = self.pk + query["_id"] = self.pk # pre_delete can provide additional query filter additional_filter = yield maybeDeferred(self.pre_delete) if additional_filter: @@ -141,8 +154,7 @@ def delete(self, conditions=None): return ret def io_validate(self, validate_all=False): - """ - Run the io_validators of the document's fields. + """Run the io_validators of the document's fields. :param validate_all: If False only run the io_validators of the fields that have been modified. @@ -150,18 +162,23 @@ def io_validate(self, validate_all=False): if validate_all: return _io_validate_data_proxy(self.schema, self._data) return _io_validate_data_proxy( - self.schema, self._data, partial=self._data.get_modified_fields()) + self.schema, + self._data, + partial=self._data.get_modified_fields(), + ) @classmethod @inlineCallbacks - def find_one(cls, filter=None, projection=None, *args, **kwargs): - """ - Find a single document in database. - """ + def find_one(cls, filter=None, projection=None, **kwargs): + """Find a single document in database.""" filter = cook_find_filter(cls, filter) if projection: projection = cook_find_projection(cls, projection) - ret = yield cls.collection.find_one(filter, projection=projection, *args, **kwargs) + ret = yield cls.collection.find_one( + filter, + projection=projection, + **kwargs, + ) if ret is not None: ret = cls.build_from_mongo(ret, use_cls=True) return ret @@ -169,8 +186,7 @@ def find_one(cls, filter=None, projection=None, *args, **kwargs): @classmethod @inlineCallbacks def find(cls, filter=None, *args, **kwargs): - """ - Find a list document in database. + """Find a list document in database. Returns a list of Documents. """ @@ -181,13 +197,16 @@ def find(cls, filter=None, *args, **kwargs): @classmethod @inlineCallbacks def find_with_cursor(cls, filter=None, *args, **kwargs): - """ - Find a list document in database. + """Find a list document in database. Returns a cursor that provides Documents. """ filter = cook_find_filter(cls, filter) - raw_cursor_or_list = yield cls.collection.find_with_cursor(filter, *args, **kwargs) + raw_cursor_or_list = yield cls.collection.find_with_cursor( + filter, + *args, + **kwargs, + ) def wrap_raw_results(result): cursor = result[1] @@ -199,27 +218,22 @@ def wrap_raw_results(result): @classmethod def count(cls, filter=None, **kwargs): - """ - Get the number of documents in this collection. - """ + """Get the number of documents in this collection.""" filter = cook_find_filter(cls, filter) return cls.collection.count(filter=filter, **kwargs) @classmethod @inlineCallbacks def ensure_indexes(cls): - """ - Check&create if needed the Document's indexes in database - """ + """Check&create if needed the Document's indexes in database""" for index in cls.indexes: kwargs = index.document.copy() - keys = kwargs.pop('key') - index = qf.sort(keys.items()) + keys = kwargs.pop("key") + index = qf.sort(keys) yield cls.collection.create_index(index, **kwargs) def _errback_factory(errors, field=None, subkey=None): - def errback(err): if isinstance(err.value, ma.ValidationError): error = err.value.messages @@ -248,7 +262,9 @@ def _run_validators(validators, field, value): else: if defer is None: continue - assert isinstance(defer, Deferred), 'io_validate functions must return a Deferred' + assert isinstance(defer, Deferred), ( + "io_validate functions must return a Deferred" + ) defer.addErrback(_errback_factory(errors)) defers.append(defer) yield DeferredList(defers) @@ -325,12 +341,11 @@ def _dict_io_validate(field, value): def _embedded_document_io_validate(field, value): if not value: - return + return None return _io_validate_data_proxy(value.schema, value._data) class TxMongoReference(Reference): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._document = None @@ -339,16 +354,18 @@ def __init__(self, *args, **kwargs): def fetch(self, no_data=False, force_reload=False, projection=None): if not self._document or force_reload: if self.pk is None: - raise NoneReferenceError('Cannot retrieve a None Reference') + raise NoneReferenceError("Cannot retrieve a None Reference") self._document = yield self.document_cls.find_one(self.pk, projection) if not self._document: - raise ma.ValidationError(self.error_messages['not_found'].format( - document=self.document_cls.__name__)) - returnValue(self._document) + raise ma.ValidationError( + self.error_messages["not_found"].format( + document=self.document_cls.__name__, + ), + ) + return self._document class TxMongoBuilder(BaseBuilder): - BASE_DOCUMENT_CLS = TxMongoDocument def _patch_field(self, field): @@ -358,7 +375,7 @@ def _patch_field(self, field): if not validators: field.io_validate = [] else: - if hasattr(validators, '__iter__'): + if hasattr(validators, "__iter__"): validators = list(validators) else: validators = [validators] @@ -375,9 +392,8 @@ def _patch_field(self, field): class TxMongoInstance(Instance): - """ - :class:`umongo.instance.Instance` implementation for txmongo - """ + """:class:`umongo.instance.Instance` implementation for txmongo""" + BUILDER_CLS = TxMongoBuilder @staticmethod @@ -395,7 +411,8 @@ def migrate_2_to_3(self): - EmbeddedDocument _cls field is only set if child of concrete embedded document """ concrete_not_children = [ - name for name, ed in self._embedded_lookup.items() + name + for name, ed in self._embedded_lookup.items() if not ed.opts.is_child and not ed.opts.abstract ] diff --git a/umongo/i18n.py b/src/umongo/i18n.py similarity index 60% rename from umongo/i18n.py rename to src/umongo/i18n.py index 727cc568..cf660aad 100644 --- a/umongo/i18n.py +++ b/src/umongo/i18n.py @@ -2,8 +2,7 @@ def gettext(message): - """ - Return the localized translation of message. + """Return the localized translation of message. .. note:: If :func:`set_gettext` is not called prior, this function retuns the message unchanged @@ -12,18 +11,17 @@ def gettext(message): def set_gettext(gettext): - """ - Define a function that will be used to localize messages. + """Define a function that will be used to localize messages. - .. note:: Most common function to use for this would be default :func:`gettext.gettext` + .. note:: Most common function to use for this would be default + :func:`gettext.gettext` """ - global _gettext + global _gettext # noqa: PLW0603 _gettext = gettext -def N_(message): - """ - Dummy function to mark strings as translatable for babel indexing. +def N_(message): # noqa: N802 + """Dummy function to mark strings as translatable for babel indexing. see https://docs.python.org/3.5/library/gettext.html#deferred-translations """ return message diff --git a/umongo/indexes.py b/src/umongo/indexes.py similarity index 53% rename from umongo/indexes.py rename to src/umongo/indexes.py index 749cca15..d20f9cb2 100644 --- a/umongo/indexes.py +++ b/src/umongo/indexes.py @@ -1,17 +1,17 @@ -from pymongo import IndexModel, ASCENDING, DESCENDING, TEXT, HASHED +from pymongo import ASCENDING, DESCENDING, HASHED, TEXT, IndexModel def explicit_key(index): if isinstance(index, (list, tuple)): - assert len(index) == 2, 'Must be a (`key`, `direction`) tuple' + assert len(index) == 2, "Must be a (`key`, `direction`) tuple" return index - if index.startswith('+'): + if index.startswith("+"): return (index[1:], ASCENDING) - if index.startswith('-'): + if index.startswith("-"): return (index[1:], DESCENDING) - if index.startswith('$'): + if index.startswith("$"): return (index[1:], TEXT) - if index.startswith('#'): + if index.startswith("#"): return (index[1:], HASHED) return (index, ASCENDING) @@ -20,20 +20,22 @@ def parse_index(index, base_compound_field=None): keys = None args = {} if isinstance(index, IndexModel): - keys = index.document['key'].items() - args = {k: v for k, v in index.document.items() if k != 'key'} + keys = index.document["key"].items() + args = {k: v for k, v in index.document.items() if k != "key"} elif isinstance(index, (tuple, list)): # Compound indexes keys = [explicit_key(e) for e in index] elif isinstance(index, str): keys = [explicit_key(index)] elif isinstance(index, dict): - assert 'key' in index, 'Index passed as dict must have a `key` entry' - assert hasattr(index['key'], '__iter__'), '`key` entry must be iterable' - keys = [explicit_key(e) for e in index['key']] - args = {k: v for k, v in index.items() if k != 'key'} + assert "key" in index, "Index passed as dict must have a `key` entry" + assert hasattr(index["key"], "__iter__"), "`key` entry must be iterable" + keys = [explicit_key(e) for e in index["key"]] + args = {k: v for k, v in index.items() if k != "key"} else: - raise TypeError('Index type must be , , or ') + raise TypeError( + "Index type must be , , or ", + ) if base_compound_field: keys.append(explicit_key(base_compound_field)) return IndexModel(keys, **args) diff --git a/umongo/instance.py b/src/umongo/instance.py similarity index 80% rename from umongo/instance.py rename to src/umongo/instance.py index b40efeff..a76e6fc6 100644 --- a/umongo/instance.py +++ b/src/umongo/instance.py @@ -1,15 +1,17 @@ import abc -from .exceptions import ( - NotRegisteredDocumentError, AlreadyRegisteredDocumentError, NoDBDefinedError) from .document import DocumentTemplate from .embedded_document import EmbeddedDocumentTemplate +from .exceptions import ( + AlreadyRegisteredDocumentError, + NoDBDefinedError, + NotRegisteredDocumentError, +) from .template import get_template class Instance(abc.ABC): - """ - Abstract instance class + """Abstract instance class Instances aims at collecting and implementing :class:`umongo.template.Template`:: @@ -17,6 +19,7 @@ class Instance(abc.ABC): class Doc(DocumentTemplate): pass + instance = MyFrameworkInstance() # doc_cls is the instance's implementation of Doc doc_cls = instance.register(Doc) @@ -29,6 +32,7 @@ class Doc(DocumentTemplate): Instance registration is divided between :class:`umongo.Document` and :class:`umongo.EmbeddedDocument`. """ + BUILDER_CLS = None def __init__(self, db=None): @@ -42,27 +46,28 @@ def __init__(self, db=None): @classmethod def from_db(cls, db): - from .frameworks import find_instance_from_db + from .frameworks import find_instance_from_db # noqa: PLC0415 + instance_cls = find_instance_from_db(db) instance = instance_cls() instance.set_db(db) return instance def retrieve_document(self, name_or_template): - """ - Retrieve a :class:`umongo.document.DocumentImplementation` registered into this - instance from it name or it template class (i.e. :class:`umongo.Document`). + """Retrieve a :class:`umongo.document.DocumentImplementation` registered + into this instance from its name or its template class + (i.e. :class:`umongo.Document`). """ if not isinstance(name_or_template, str): name_or_template = name_or_template.__name__ if name_or_template not in self._doc_lookup: raise NotRegisteredDocumentError( - 'Unknown document class "%s"' % name_or_template) + f'Unknown document class "{name_or_template}"' + ) return self._doc_lookup[name_or_template] def retrieve_embedded_document(self, name_or_template): - """ - Retrieve a :class:`umongo.embedded_document.EmbeddedDocumentImplementation` + """Retrieve a :class:`umongo.embedded_document.EmbeddedDocumentImplementation` registered into this instance from it name or it template class (i.e. :class:`umongo.EmbeddedDocument`). """ @@ -70,12 +75,12 @@ def retrieve_embedded_document(self, name_or_template): name_or_template = name_or_template.__name__ if name_or_template not in self._embedded_lookup: raise NotRegisteredDocumentError( - 'Unknown embedded document class "%s"' % name_or_template) + f'Unknown embedded document class "{name_or_template}"', + ) return self._embedded_lookup[name_or_template] def register(self, template): - """ - Generate an :class:`umongo.template.Implementation` from the given + """Generate an :class:`umongo.template.Implementation` from the given :class:`umongo.template.Template` for this instance. :param template: :class:`umongo.template.Template` to implement @@ -90,10 +95,12 @@ class you defined:: class MyEmbedded(EmbeddedDocument): pass + @instance.register class MyDoc(Document): emb = fields.EmbeddedField(MyEmbedded) + MyDoc.find() """ @@ -111,7 +118,8 @@ def _register_doc(self, template): implementation = self.builder.build_from_template(template) if implementation.__name__ in self._doc_lookup: raise AlreadyRegisteredDocumentError( - 'Document `%s` already registered' % implementation.__name__) + f'Document "{implementation.__name__}" already registered' + ) self._doc_lookup[implementation.__name__] = implementation return implementation @@ -119,7 +127,8 @@ def _register_embedded_doc(self, template): implementation = self.builder.build_from_template(template) if implementation.__name__ in self._embedded_lookup: raise AlreadyRegisteredDocumentError( - 'EmbeddedDocument `%s` already registered' % implementation.__name__) + f'EmbeddedDocument "{implementation.__name__}" already registered' + ) self._embedded_lookup[implementation.__name__] = implementation return implementation @@ -127,14 +136,15 @@ def _register_mixin_doc(self, template): implementation = self.builder.build_from_template(template) if implementation.__name__ in self._mixin_lookup: raise AlreadyRegisteredDocumentError( - 'MixinDocument `%s` already registered' % implementation.__name__) + f'MixinDocument "{implementation.__name__}" already registered' + ) self._mixin_lookup[implementation.__name__] = implementation return implementation @property def db(self): if self._db is None: - raise NoDBDefinedError('db not set, please call set_db') + raise NoDBDefinedError("db not set, please call set_db") return self._db @abc.abstractmethod @@ -142,8 +152,7 @@ def is_compatible_with(self, db): return NotImplemented def set_db(self, db): - """ - Set the database to use whithin this instance. + """Set the database to use whithin this instance. .. note:: The documents registered in the instance cannot be used diff --git a/umongo/marshmallow_bonus.py b/src/umongo/marshmallow_bonus.py similarity index 68% rename from umongo/marshmallow_bonus.py rename to src/umongo/marshmallow_bonus.py index 930ec116..04498c77 100644 --- a/umongo/marshmallow_bonus.py +++ b/src/umongo/marshmallow_bonus.py @@ -1,14 +1,15 @@ """Pure marshmallow fields used in umongo""" -import bson + import marshmallow as ma -from .i18n import gettext as _ +import bson +from .i18n import gettext as _ __all__ = ( - 'ObjectId', - 'Reference', - 'GenericReference' + "GenericReference", + "ObjectId", + "Reference", ) @@ -23,8 +24,8 @@ def _serialize(self, value, attr, obj): def _deserialize(self, value, attr, data, **kwargs): try: return bson.ObjectId(value) - except (TypeError, bson.errors.InvalidId): - raise ma.ValidationError(_('Invalid ObjectId.')) + except (TypeError, bson.errors.InvalidId) as exc: + raise ma.ValidationError(_("Invalid ObjectId.")) from exc class Reference(ObjectId): @@ -49,16 +50,18 @@ def _serialize(self, value, attr, obj): # In OO world, value is a :class:`umongo.data_object.Reference` # or a dict before being loaded into a Document if isinstance(value, dict): - return {'id': str(value['id']), 'cls': value['cls']} - return {'id': str(value.pk), 'cls': value.document_cls.__name__} + return {"id": str(value["id"]), "cls": value["cls"]} + return {"id": str(value.pk), "cls": value.document_cls.__name__} def _deserialize(self, value, attr, data, **kwargs): if not isinstance(value, dict): raise ma.ValidationError(_("Invalid value for generic reference field.")) - if value.keys() != {'cls', 'id'}: - raise ma.ValidationError(_("Generic reference must have `id` and `cls` fields.")) + if value.keys() != {"cls", "id"}: + raise ma.ValidationError( + _("Generic reference must have `id` and `cls` fields."), + ) try: - _id = bson.ObjectId(value['id']) - except ValueError: - raise ma.ValidationError(_("Invalid `id` field.")) - return {'cls': value['cls'], 'id': _id} + _id = bson.ObjectId(value["id"]) + except ValueError as exc: + raise ma.ValidationError(_("Invalid `id` field.")) from exc + return {"cls": value["cls"], "id": _id} diff --git a/umongo/mixin.py b/src/umongo/mixin.py similarity index 67% rename from umongo/mixin.py rename to src/umongo/mixin.py index c43483fe..bb390bbc 100644 --- a/umongo/mixin.py +++ b/src/umongo/mixin.py @@ -1,16 +1,16 @@ """umongo MixinDocument""" + from .template import Implementation, Template __all__ = ( - 'MixinDocumentTemplate', - 'MixinDocument', - 'MixinDocumentImplementation' + "MixinDocument", + "MixinDocumentImplementation", + "MixinDocumentTemplate", ) class MixinDocumentTemplate(Template): - """ - Base class to define a umongo mixin document. + """Base class to define a umongo mixin document. .. note:: Once defined, this class must be registered inside a @@ -24,8 +24,7 @@ class MixinDocumentTemplate(Template): class MixinDocumentOpts: - """ - Configuration for an :class:`umongo.mixin.MixinDocument`. + """Configuration for an :class:`umongo.mixin.MixinDocument`. ==================== ====================== =========== attribute configurable in Meta description @@ -34,11 +33,13 @@ class MixinDocumentOpts: instance no Implementation's instance ==================== ====================== =========== """ + def __repr__(self): - return ('<{ClassName}(' - 'instance={self.instance}, ' - 'template={self.template}, ' - .format(ClassName=self.__class__.__name__, self=self)) + return ( + f"<{self.__class__.__name__}(" + f"instance={self.instance}, " + f"template={self.template}, " + ) def __init__(self, instance, template): self.instance = instance @@ -46,11 +47,11 @@ def __init__(self, instance, template): class MixinDocumentImplementation(Implementation): - """ - Represent a mixin document once it has been implemented inside a + """Represent a mixin document once it has been implemented inside a :class:`umongo.instance.BaseInstance`. """ + opts = MixinDocumentOpts(None, MixinDocumentTemplate) def __repr__(self): - return '' % (self.__module__, self.__class__.__name__) + return f"" diff --git a/umongo/query_mapper.py b/src/umongo/query_mapper.py similarity index 78% rename from umongo/query_mapper.py rename to src/umongo/query_mapper.py index 098271be..c9710a2d 100644 --- a/umongo/query_mapper.py +++ b/src/umongo/query_mapper.py @@ -1,11 +1,10 @@ -from umongo.fields import ListField, EmbeddedField from umongo.document import DocumentImplementation from umongo.embedded_document import EmbeddedDocumentImplementation +from umongo.fields import EmbeddedField, ListField def map_entry(entry, fields): - """ - Retrieve the entry from the given fields and replace it if it should + """Retrieve the entry from the given fields and replace it if it should have a different name within the database. :param entry: is one of the followings: @@ -19,23 +18,20 @@ def map_entry(entry, fields): fields = field.inner.embedded_document_cls.schema.fields elif isinstance(field, EmbeddedField): fields = field.embedded_document_cls.schema.fields - return getattr(field, 'attribute', None) or entry, fields + return getattr(field, "attribute", None) or entry, fields def map_entry_with_dots(entry, fields): - """ - Consider the given entry can be a '.' separated combination of single entries. - """ + """Consider the given entry can be a '.' separated combination of single entries.""" mapped = [] - for sub_entry in entry.split('.'): + for sub_entry in entry.split("."): mapped_sub_entry, fields = map_entry(sub_entry, fields) mapped.append(mapped_sub_entry) - return '.'.join(mapped), fields + return ".".join(mapped), fields def map_query(query, fields): - """ - Retrieve given fields within the query and replace their names with + """Retrieve given fields within the query and replace their names with the names they should have within the database. """ if isinstance(query, dict): diff --git a/umongo/template.py b/src/umongo/template.py similarity index 72% rename from umongo/template.py rename to src/umongo/template.py index 8bed6f25..744197bd 100644 --- a/umongo/template.py +++ b/src/umongo/template.py @@ -2,7 +2,6 @@ class MetaTemplate(type): - def __new__(cls, name, bases, nmspc): # If user has passed parent documents as implementation, we need # to retrieve the original templates @@ -14,43 +13,41 @@ def __new__(cls, name, bases, nmspc): return type.__new__(cls, name, tuple(cooked_bases), nmspc) def __repr__(cls): - return "