diff --git a/.gitattributes b/.gitattributes index 2c53400b..4aa54ca2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,3 +17,5 @@ *.mo binary *.png binary + +.git_archival.txt export-subst diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 00000000..7cb567a4 --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,88 @@ +name: Publish Python distribution to PyPI + +on: push + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install pypa/build + run: python3 -m pip install build --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + publish-to-pypi: + name: Publish Python 🐍 distribution 📦 to PyPI + if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes + needs: + - build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/FormEncode + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + github-release: + name: >- + Sign the Python distribution with Sigstore + and upload them to GitHub Release + needs: + - publish-to-pypi + runs-on: ubuntu-latest + + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + id-token: write # IMPORTANT: mandatory for sigstore + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Sign the dists with Sigstore + uses: sigstore/gh-action-sigstore-python@v3.0.0 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + gh release create + "$GITHUB_REF_NAME" + --repo "$GITHUB_REPOSITORY" + --notes "" + - name: Upload artifact signatures to GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + # Upload to GitHub Release using the `gh` CLI. + # `dist/` contains the built packages, and the + # sigstore-produced signatures and certificates. + run: >- + gh release upload + "$GITHUB_REF_NAME" dist/** + --repo "$GITHUB_REPOSITORY" diff --git a/.github/workflows/run-qa.yml b/.github/workflows/run-qa.yml new file mode 100644 index 00000000..0946cd2b --- /dev/null +++ b/.github/workflows/run-qa.yml @@ -0,0 +1,22 @@ +name: Code quality + +on: + push: + branches: + - main + pull_request: {} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: '3.12' + - name: Install dependencies + run: pip install flake8 + - name: Run code quality checks + run: make flake8 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 00000000..602ac332 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,43 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: {} + +jobs: + build: + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + os: [ubuntu-latest, macOS-latest, windows-latest] + include: + # pypy3 on Windows currently fails trying to + # run dnspython tests. Moving pypy3 to only test linux. + - python-version: 'pypy3.10' + os: ubuntu-latest + - python-version: '3.7' + os: ubuntu-22.04 + - python-version: '3.7' + os: macos-13 + exclude: + - python-version: '3.7' + os: macOS-latest + - python-version: '3.7' + os: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: make + - name: Run tests + run: make tests diff --git a/.gitignore b/.gitignore index 841a9115..619c6219 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,14 @@ *.egg *.egg-info +.eggs + *.pyc *.pyo -*.tox + +.tox + +.coverage +coverage/ build dist/ @@ -13,6 +19,4 @@ docs/_build .project .pydevproject .settings - -.coverage -coverage/ +.tool-versions diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..3dd84c58 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: requirements-docs.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7cbe05a1..00000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: python - -python: - - 2.6 - - 2.7 - - 3.2 - - 3.3 - - pypy - -install: python setup.py install - -script: python setup.py test - diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt new file mode 100644 index 00000000..7be099e6 --- /dev/null +++ b/CONTRIBUTORS.txt @@ -0,0 +1,118 @@ +List of Contributors +==================== + +The below-signed are contributors to a code repository that is part of the +project named "FormEncode". Each below-signed contributor has agreed to +license their contributions under the MIT license (see LICENSE.txt) as of the +date listed by their name. + +Contributors +------------ + - Ian Bicking, 2015-11-14 + + - Christoph Zwerschke, 2015-11-14 + + - Christopher Lambacher, 2015-11-14 + + - Philip Jenvey, 2015-11-14 + + - Ian Wilson, 2015-11-14 + + - W. Mark Kubacki, 2015-11-14 + + - Greg Horvath, 2015-11-14 + + - Maxim Avanov, 2015-11-14 + + - Ben Bangert, 2015-11-14 + + - Matthew Desmarais, 2015-11-14 + + - Ethan Jucovy, 2015-11-14 + + - Marc Abramowitz, 2015-11-14 + + - Benjamin Behringer, 2015-11-14 + + - Richard Flosi, 2015-11-14 + + - Juliusz Gonera, 2015-11-14 + + - John Kraal, 2015-11-14 + + - Alessandro Molina, 2015-11-14 + + - Neil Muller, 2015-11-14 + + - Philipp Spitzer, 2015-11-14 + + - Vitaly Babiy, 2015-11-14 + + - David Stanek, 2015-11-14 + + - Ira, 2015-11-14 + + - Hong Minhee, 2015-11-14 + + - Felix Wolfsteller, 2015-11-14 + + - Keith Yang, 2015-11-14 + + - Elmer de Looff, 2015-11-14 + + - Jonathan Vanasco, 2015-11-14 + + - Luke Tucker, 2015-11-14 + + - Kevin Dangoor, 2015-11-14 + + - Mark Lipscombe, 2015-11-14 + + - Nina Plakalovic, 2015-11-14 + + - Oleg Broytman, 2015-11-14 + + - Christophe Benz, 2015-11-14 + + - Chris George, 2015-11-14 + + - Philipp Hagemeister, 2015-11-14 + + - Remco Verhoef, 2015-11-14 + + - Ryan P. Kelly, 2015-11-14 + + - Jason R. Coombs, 2015-11-14 + + - Joshua Bronson, 2015-11-14 + + - Aleksey Sivokon, 2015-11-14 + + - Stéphane Raimbault 2015-11-23 + + - Jason Culverhouse, 2015-11-24 + + - Michele Cella, 2015-11-30 + + - Arnaud Fontaine, 2015-12-01 + + - Bradley Arsenault, 2015-12-05 + + - Bo Du, 2015-12-28 + + - Miloš Gavrilov, 2015-12-06 + + +The following people made their contributions under the PSF license and did +not respond to requests to approve the change to the MIT license. If you are +one of these contributors and you do not want you changes licensed under MIT +please contact the project managers and we will work on removing your +contributions. + + - Patrick Hunt + + - Dennis Muhlestein + + - Marius Gedminas + + - Jacob Smullyan diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..20bd89b8 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2015 Ian Bicking and FormEncode Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 8e84270e..549bdeb8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,26 @@ -recursive-include . *.py -recursive-include docs *.html *.txt *.css -recursive-include formencode/javascript *.js -recursive-include formencode/i18n *.py *.pot *.po *.mo -recursive-include formencode/tests *.txt -recursive-exclude . *.pyc *.pyo *~ +exclude .gitignore +exclude .gitattributes +exclude .readthedocs.yaml + +include MANIFEST.in + +include LICENSE.txt +include README.rst + +include tox.ini + +graft src/formencode +graft src/formencode/javascript +include src/formencode/i18n/*.pot +recursive-include src/formencode/i18n *.po *.mo + +graft examples +recursive-include examples *.py + +graft tests +graft tests/htmlfill_data +recursive-include docs *.rst conf.py Makefile make.bat prune docs/_build -prune **/.svn -prune .hg -prune .git + +global-exclude */__pycache__/* +global-exclude *.py[cod] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..4a5d06db --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +init: + pip install -e . + pip install -r requirements-test.txt + +.PHONY: tests + +tests: + pytest tests + +flake8: + flake8 src tests + +coverage: + pytest --cov-config .coveragerc --verbose --cov-report term --cov-report xml --cov=formencode formencode + +.PHONY: docs + +docs: + cd docs && make html + @echo "\033[95m\n\nBuild successful! View the docs homepage at docs/_build/html/index.html.\n\033[0m" diff --git a/README.rst b/README.rst index 335b4ef3..76d97684 100644 --- a/README.rst +++ b/README.rst @@ -1,30 +1,46 @@ FormEncode ========== -.. image:: https://secure.travis-ci.org/formencode/formencode.png?branch=master - :target: https://travis-ci.org/formencode/formencode - :alt: Travis CI continuous integration status +|PyPI| |Python| |Tests| + +.. |PyPI| image:: https://img.shields.io/pypi/v/formencode + :target: https://pypi.org/project/formencode + :alt: PyPI + +.. |Python| image:: https://img.shields.io/pypi/pyversions/formencode + :target: https://pypi.org/project/formencode + :alt: PyPI - Python Version + +.. |Tests| image:: https://github.com/formencode/formencode/actions/workflows/run-tests.yml/badge.svg + :target: https://github.com/formencode/formencode/actions + :alt: Test Status Introduction ------------ -FormEncode is a validation and form generation package. The -validation can be used separately from the form generation. The -validation works on compound data structures, with all parts being -nestable. It is separate from HTTP or any other input mechanism. +FormEncode is a validation and form generation package. +The validation can be used separately from the form generation. +The validation works on compound data structures, with all parts being nestable. +It is separate from HTTP or any other input mechanism. Documentation ------------- -The latest documentation is available at http://www.formencode.org/ +The latest documentation is available at +`www.formencode.org `_. -Changes + +Testing ------- -Added a validator that can require one or more fields based on the value of another field. +Use ``pytest formencode`` to run the test suite. +Use ``tox`` to run the test suite for all supported Python versions. + -A german howto can be found here: http://techblog.auf-nach-mallorca.info/2014/08/19/dynamische_formulare_validieren_mit_formencode/ +Changes +------- -Courtesy of the developers of http://www.auf-nach-mallorca.info +See the `What's New `_ +section of the documentation. diff --git a/docs/Validator.txt b/docs/Validator.txt index 91e68deb..7dc671c1 100644 --- a/docs/Validator.txt +++ b/docs/Validator.txt @@ -35,9 +35,9 @@ The basic metaphor for validation is **to_python** and -- the trusted application, your own Python objects. The "other" may be a web form, an external database, an XML-RPC request, or any data source that is not completely trusted or does not map directly to -Python's object model. :meth:`to_python` is the process of taking -external data and preparing it for internal use, :meth:`from_python` -generally reverses this process (:meth:`from_python` is usually the less +Python's object model. ``to_python()`` is the process of taking +external data and preparing it for internal use, ``from_python()`` +generally reverses this process (``from_python()`` is usually the less interesting of the pair, but provides some important features). The core of this validation process is two methods and an exception:: @@ -52,31 +52,29 @@ The core of this validation process is two methods and an exception:: ... Invalid: Please enter an integer value -``"ten"`` isn't a valid integer, so we get a :class:`formencode.Invalid` -exception. Typically we'd catch that exception, and use it for some +``"ten"`` isn't a valid integer, so we get a :class:`~formencode.api.Invalid` +exception. Typically, we'd catch that exception, and use it for some sort of feedback. Like: -.. comment (fake raw_input): +.. comment (fake input): - >>> raw_input_input = [] - >>> def raw_input(prompt): - ... value = raw_input_input.pop(0) - ... print '%s%s' % (prompt, value) + >>> input_values = [] + >>> def input(prompt): + ... value = input_values.pop(0) + ... print ('%s%s' % (prompt, value)) ... return value - >>> raw_input_input.extend(['ten', '10']) - >>> raw_input_input.extend(['bob', 'bob@nowhere.com']) - >>> if unicode is str: # Python 3 - ... input = raw_input + >>> input_values.extend(['ten', '10']) + >>> input_values.extend(['bob', 'bob@nowhere.com']) :: >>> def get_integer(): ... while 1: ... try: - ... value = raw_input('Enter a number: ') + ... value = input('Enter a number: ') ... return validator.to_python(value) - ... except formencode.Invalid, e: - ... print e + ... except formencode.Invalid as e: + ... print (e) ... >>> get_integer() Enter a number: ten @@ -89,17 +87,17 @@ We can also generalize this kind of function:: >>> def valid_input(prompt, validator): ... while 1: ... try: - ... value = raw_input(prompt) + ... value = input(prompt) ... return validator.to_python(value) - ... except formencode.Invalid, e: - ... print e + ... except formencode.Invalid as e: + ... print(e) >>> valid_input('Enter your email: ', validators.Email()) Enter your email: bob An email address must contain a single @ Enter your email: bob@nowhere.com 'bob@nowhere.com' -:class:`Invalid` exceptions generally give a good, user-readable error +``Invalid`` exceptions generally give a good, user-readable error message about the problem with the input. Using the exception gets more complicated when you use compound data structures (dictionaries and lists), which we'll talk about later__. @@ -150,24 +148,26 @@ they'll be specific to your application:: ... return value .. note:: - The class :class:`formencode.FancyValidator` is the superclass + The class `formencode.FancyValidator` is the superclass for most validators in FormEncode, and it provides a number of useful features that most validators can use -- for instance, you can pass ``strip=True`` into any of these validators, and they'll strip whitespace from the incoming value before any other validation. -This overrides the internal :meth:`_convert_to_python()` method: -:class:`formencode.FancyValidator` adds a number of extra features, and then -calls the internal :meth:`_convert_to_python` method, which is the method you'll -typically write. Contrary to the external method :meth:`to_python`, its -only concern is the conversion part, not the validation part. If further -validation is necessary, this can be done in two other internal methods, -either :meth:`_validate_python()` or :meth:`_validate_other()`. We will give -an example for that later. When a validator finds an error, it raises an -exception (:class:`formencode.Invalid`), with the error message and the -value and "state" objects. We'll talk about state_ later. Here's the -other custom validator, that checks passwords against words in the -standard Unix word file:: +This overrides the internal ``_convert_to_python()`` method: +:class:`formencode.api.FancyValidator` adds a number of extra features, +and then calls the internal ``_convert_to_python`` method, +which is the method you'll typically write. +Contrary to the external method ``to_python()``, its only concern is +the conversion part, not the validation part. +If further validation is necessary, +this can be done in two other internal methods, +either ``_validate_python()`` or ``_validate_other()``. +We will give an example for that later. When a validator finds an error, +it raises an exception (:class:`formencode.api.Invalid`), with the error +message and the value and "state" objects. We'll talk about state_ later. +Here's the other custom validator, that checks passwords against words +in the standard Unix word file:: >>> class SecurePassword(formencode.FancyValidator): ... words_filename = '/usr/share/dict/words' @@ -194,8 +194,8 @@ And here's a schema:: ... chained_validators = [validators.FieldsMatch( ... 'password', 'password_confirm')] -Like any other validator, a :class:`Registration` instance will have the -:meth:`to_python` and :meth:`from_python` methods. The input should be a +Like any other validator, a ``Registration`` instance will have the +``to_python`` and ``from_python`` methods. The input should be a dictionary (or a Paste MultiDict), with keys like ``"first_name"``, ``"password"``, etc. The validators you give as attributes will be applied to each of the values of the dictionary. *All* the values @@ -203,26 +203,27 @@ will be validated, so if there are multiple invalid fields you will get information about all of them. Most validators (anything that subclasses -:class:`formencode.FancyValidator`) will take a certain standard -set of constructor keyword arguments. See -:class:`formencode.api.FancyValidator` for more -- here we use -``not_empty=True``. +:class:`formencode.api.FancyValidator`) will take a certain standard +set of constructor keyword arguments. +See :class:`formencode.api.FancyValidator` for more +-- here we use ``not_empty=True``. -Another notable validator is :class:`formencode.compound.All` -- this +Another notable validator is :class:`~formencode.compound.All` -- this is a *compound validator* -- that is, it's a validator that takes -validators as input. Schemas are one example; in this case :class:`All` -takes a list of validators and applies each of them in turn. -:class:`formencode.compound.Any` is its compliment, that uses the +validators as input. Schemas are one example; in this case +:class:`~formencode.compound.All` takes a list of validators +and applies each of them in turn. +:class:`~formencode.compound.Any` is its compliment, that uses the first passing validator in its list. .. _pre_validators: .. _chained_validators: -:attr:`chained_validators` are validators that are run on the entire -dictionary after other validation is done (:attr:`pre_validators` are -applied before the schema validation). chained_validators will also +``chained_validators`` are validators that are run on the entire +dictionary after other validation is done (``pre_validators`` are +applied before the schema validation). ``chained_validators`` will also allow for multiple validators to fail and report to the error_dict -so, for example, if you have an email_confirm and a password_confirm +so, for example, if you have an ``email_confirm`` and a ``password_confirm`` fields and use FieldsMatch on both of them as follows: >>> chained_validators = [ @@ -257,16 +258,17 @@ function that you write. For example:: Invalid: state: You must enter a state -The :func:`validate_state` function (or any validation function) returns +The ``validate_state()`` function (or any validation function) returns any errors in the form (or it may raise Invalid directly). It can -also modify the :obj:`value_dict` dictionary directly. When it returns +also modify the ``value_dict`` dictionary directly. When it returns None this indicates that everything is valid. You can use this with a -:class:`Schema` by putting :class:`ValidateState` in :attr:`pre_validators` -(all validation will be done before the schema's validation, and if there's -an error the schema won't be run). Or you can put it in -:attr:`chained_validators` and it will be run *after* the schema. If the -schema fails (the values are invalid) then :class:`ValidateState` will not -be run, unless you set :attr:`validate_partial_form` to True (like +:class:`formencode.schema.Schema` by putting ``ValidateState`` in +``pre_validators`` (all validation will be done before the schema's +validation, and if there's an error the schema won't be run). +Or you can put it in ``chained_validators`` and it will be run +*after* the schema. If the schema fails (the values are invalid) +then ``ValidateState`` will not be run, unless you set +``validate_partial_form`` to True (like ``ValidateState = SimpleFormValidator(validate_state, validate_partial_form=True)``. If you validate a partial form you should be careful that you handle missing keys and other @@ -285,12 +287,12 @@ is for:: ... title = validators.ByteString(not_empty=True) >>> validator = formencode.ForEach(BookSchema()) -The :obj:`validator` we've created will take a list of dictionaries as +The ``validator`` we've created will take a list of dictionaries as input (like ``[{"id": "1", "title": "War & Peace"}, {"id": "2", -"title": "Brave New World"}, ...]``). It applies the :class:`BookSchema` -to each entry, and collects any errors and reraises them. Of course, -when you are validating input from an HTML form you won't get well -structured data like this (we'll talk about that later__). +"title": "Brave New World"}, ...]``). It applies the ``BookSchema`` +to each entry, and collects any errors and re-raises them. +Of course, when you are validating input from an HTML form you won't +get well-structured data like this (we'll talk about that later__). .. __: `HTTP/HTML Form Input`_ @@ -298,9 +300,8 @@ Writing Your Own Validator -------------------------- We gave a brief introduction to creating a validator earlier -(:class:`UniqueUsername` and :class:`SecurePassword`). We'll discuss -that a little more. Here's a more complete implementation of -:class:`SecurePassword`:: +(``UniqueUsername`` and ``SecurePassword``). We'll discuss that a little +more. Here's a more complete implementation of ``SecurePassword``:: >>> import re >>> class SecurePassword(validators.FancyValidator): @@ -324,37 +325,37 @@ that a little more. Here's a more complete implementation of ... ... def _validate_python(self, value, state): ... if len(value) < self.min: - ... raise Invalid(self.message("too_few", state, - ... min=self.min), - ... value, state) + ... raise formencode.Invalid(self.message("too_few", state, + ... min=self.min), + ... value, state) ... non_letters = self.letter_regex.sub('', value) ... if len(non_letters) < self.non_letter: - ... raise Invalid(self.message("non_letter", state, - ... non_letter=self.non_letter), - ... value, state) + ... raise formencode.Invalid(self.message("non_letter", state, + ... non_letter=self.non_letter), + ... value, state) With all validators, any arguments you pass to the constructor will be -used to set instance variables. So :class:`SecureValidator(min=5)` will +used to set instance variables. So ``SecureValidator(min=5)`` will be a minimum-five-character validator. This makes it easy to also subclass other validators, giving different default values. Unlike the previous implementation we use the already mentioned -:meth:`_validate_python` method, which is another internal method -:class:`FancyValidator` allows us to override. :meth:`_validate_python` -doesn't have any return value, it simply raises an exception if it -needs to. It validates the value *after* it has been converted -(by :meth:`_convert_to_python`). :meth:`_validate_other` validates before +``_validate_python`` method, which is another internal method +:class:`~formencode.api.FancyValidator` allows us to override. +``_validate_python`` doesn't have any return value, it simply raises an +exception if it needs to. It validates the value *after* it has been +converted (by ``_convert_to_python``). ``_validate_other`` validates before conversion, but that's usually not that useful. The external method -:meth:`to_python` cares about the extra features such as the -:attr:`if_empty` parameter, and uses the internal methods to do the -actual conversion and validation; first it calls :meth:`_validate_other`, -then :meth:`_convert_to_python` and at last :meth:`_validate_python`. +``to_python`` cares about the extra features such as the ``if_empty`` +parameter, and uses the internal methods to do the actual conversion +and validation; first it calls ``_validate_other``, then +``_convert_to_python`` and at last ``_validate_python``. The use of ``self.message(...)`` is meant to make the messages easy to -format for different environments, and replacable (with translations, +format for different environments, and replaceable (with translations, or simply with different text). Each message should have an identifier (``"min"`` and ``"non_letter"`` in this example). The -keyword arguments to :meth:`message` are used for message substitution. +keyword arguments to ``message`` are used for message substitution. See Messages_ for more. Other Validator Usage @@ -370,10 +371,9 @@ to set these. These are (effectively) equivalent:: ... regex = '^[a-zA-Z]+$' >>> plain = Plain() -You can actually use classes most places where you could use an -instance; :meth:`.to_python()` and :meth:`.from_python()` will create -instances as necessary, and many other methods are available on both -the instance and the class level. +You can actually use classes most places where you could use an instance; +``to_python()`` and ``from_python()`` will create instances as necessary, +and many other methods are available on both the instance and the class level. When dealing with nested validators this class syntax is often easier to work with, and better displays the structure. @@ -381,41 +381,39 @@ to work with, and better displays the structure. .. _FancyValidator: There are several options that most validators support (including your -own validators, if you subclass from :class:`formencode.FancyValidator`): - -:attr:`if_empty`: - If set, then this value will be returned if the input evaluates - to false (empty list, empty string, None, etc), but not the 0 or - False objects. This only applies to ``.to_python()``. - -:attr:`not_empty`: - If true, then if an empty value is given raise an error. - (Both with ``.to_python()`` and also ``.from_python()`` - if ``.validate_python`` is true). - -:attr:`strip`: - If true and the input is a string, strip it (occurs before empty - tests). - -:attr:`if_invalid`: - If set, then when this validator would raise Invalid during - ``.to_python()``, instead return this value. - -:attr:`if_invalid_python`: - If set, when the Python value (converted with - ``.from_python()``) is invalid, this value will be returned. - -:attr:`accept_python`: - If True (the default), then ``._validate_python()`` and - ``._validate_other()`` will not be called when - ``.from_python()`` is used. - -:attr:`if_missing`: - Typically when a field is missing the schema will raise an - error. In that case no validation is run -- so things like - ``if_invalid`` won't be triggered. This special attribute (if - set) will be used when the field is missing, and no error will - occur. (``None`` or ``()`` are common values) +own validators, if you subclass from :class:`formencode.api.FancyValidator`): + +``if_empty``: + If set, then this value will be returned if the input evaluates + to false (empty list, empty string, None, etc), but not the 0 or + False objects. This only applies to ``.to_python()``. + +``not_empty``: + If true, then if an empty value is given raise an error. + (Both with ``.to_python()`` and also ``.from_python()`` + if ``.validate_python`` is true). + +``strip``: + If true and the input is a string, strip it (occurs before empty tests). + +``if_invalid``: + If set, then when this validator would raise Invalid during + ``.to_python()``, instead return this value. + +``if_invalid_python``: + If set, when the Python value (converted with + ``.from_python()``) is invalid, this value will be returned. + +``accept_python``: + If True (the default), then ``._validate_python()`` and + ``._validate_other()`` will not be called when ``.from_python()`` is used. + +``if_missing``: + Typically when a field is missing the schema will raise an + error. In that case no validation is run -- so things like + ``if_invalid`` won't be triggered. This special attribute (if + set) will be used when the field is missing, and no error will + occur. (``None`` or ``()`` are common values) State ----- @@ -433,47 +431,49 @@ validator know the locale? State! Whatever else you need to pass in, just put it in the state object as an attribute, then look for that attribute in your validator. -Also, during compound validation (a :class:`formencode.schema.Schema` -or :class:`formencode.foreach.ForEach`) the state (if not None) will -have more instance variables added to it. During a :class:`Schema` -(dictionary) validation the instance variable ``key`` and -``full_dict`` will be added -- ``key`` is the current key (i.e., -validator name), and ``full_dict`` is the rest of the values being -validated. During a :class:`ForEeach` (list) validation, ``index`` and -``full_list`` will be set. +Also, during compound validation (a :class:`~formencode.schema.Schema` +or :class:`~formencode.foreach.ForEach`) the state (if not None) will +have more instance variables added to it. +During a :class:`~formencode.schema.Schema` (dictionary) validation +the instance variable ``key`` and ``full_dict`` will be added -- ``key`` +is the current key (i.e., validator name), and ``full_dict`` is the rest +of the values being validated. +During a :class:`~formencode.foreach.ForEach` (list) validation, +``index`` and ``full_list`` will be set. Invalid Exceptions ------------------ -Besides the string error message, :class:`formencode.Invalid` +Besides the string error message, :class:`~formencode.api.Invalid` exceptions have a few other instance variables: -:attr:`value`: +``value``: The input to the validator that failed. -:attr:`state`: +``state``: The associated state_. -:attr:`msg`: +``msg``: The error message (``str(exc)`` returns this) -:attr:`error_list`: - If the exception happened in a ``ForEach`` (list) validator, then - this will contain a list of ``Invalid`` exceptions. Each item - from the list will have an entry, either None for no error, or an - exception. +``error_list``: + If the exception happened in a :class:`~formencode.foreach.ForEach` + (list) validator, then this will contain a list of + :class:`~formencode.api.Invalid` exceptions. Each item from the list + will have an entry, either None for no error, or an exception. -:attr:`error_dict`: - If the exception happened in a :class:`Schema` (dictionary) validator, - then this will contain :class:`Invalid` exceptions for each failing - field. Passing fields not be included in this dictionary. +``error_dict``: + If the exception happened in a :class:`~formencode.schema.Schema` + (dictionary) validator, then this will contain + :class:`~formencode.api.Invalid` exceptions for each failing field. + Passing fields not be included in this dictionary. -:meth:`.unpack_errors()`: +``.unpack_errors()``: This method returns a set of lists and dictionaries containing - strings, for each error. It's an unpacking of :attr:`error_list`, - :attr:`error_dict` and :attr:`msg`. If you get an Invalid exception - from a :class:`Schema`, you probably want to call this method on the - exception object. + strings, for each error. It's an unpacking of ``error_list``, + ``error_dict`` and ``msg``. If you get an Invalid exception + from a :class:`~formencode.schema.Schema`, you probably want to call + this method on the exception object. .. _Messages: @@ -494,15 +494,15 @@ for each substitution. This way you can reorder or even ignore placeholders in your new message. When you are creating a validator, for maximum flexibility you should -use the :meth:`message` method, like:: +use the ``message`` method, like:: messages = { 'key': 'my message (with a %(substitution)s)', } def _validate_python(self, value, state): - raise Invalid(self.message('key', state, substitution='apples'), - value, state) + raise formencode.Invalid(self.message('key', state, substitution='apples'), + value, state) Localization of Error Messages (i18n) ------------------------------------- @@ -510,20 +510,20 @@ Localization of Error Messages (i18n) When a failed validation occurs FormEncode tries to output the error message in the appropriate language. For this it uses the standard `gettext `_ mechanism of -python. To translate the message in the appropirate message FormEncode +python. To translate the message in the appropriate message FormEncode has to find a gettext function that translates the string. The language to be translated into and the used domain is determined by the found gettext function. To serve a standard translation mechanism and to enable custom translations it looks in the following order to find a gettext (``_``) function: -1. method of the :obj:`state` object +1. method of the ``state`` object -2. function :func:`__builtin__._`. This function is only used when:: +2. function ``builtin._``. This function is only used when:: - Validator.use_builtin_gettext == True #True is default + Validator.use_builtin_gettext == True # True is default -3. formencode builtin :func:`_stdtrans` function +3. formencode builtin ``_stdtrans`` function for standalone use of FormEncode. The language to use is determined out of the locale system (see gettext documentation). Optionally you @@ -536,7 +536,7 @@ find a gettext (``_``) function: messages in the directory ``localedir/language/LC_MESSAGES/FormEncode.mo`` -4. Custom gettext function and addtional parameters +4. Custom gettext function and additional parameters If you use a custom gettext function and you want FormEncode to call your function with additional parameters you can set the @@ -572,12 +572,12 @@ Optionally you can also add a test of your language to ne = formencode.validators.NotEmpty() [...] def test_de(): - _test_lang("de", u"Bitte einen Wert eingeben") + _test_lang("de", "Bitte einen Wert eingeben") And the test for your language:: def test_(): - _test_lang("", u"") + _test_lang("", "") HTTP/HTML Form Input -------------------- diff --git a/docs/_static/formencode.png b/docs/_static/formencode.png new file mode 100644 index 00000000..6595c523 Binary files /dev/null and b/docs/_static/formencode.png differ diff --git a/docs/conf.py b/docs/conf.py index 4bb92a93..ac4a06dd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # FormEncode documentation build configuration file, created by # sphinx-quickstart on Tue Aug 30 2011. # @@ -11,12 +9,10 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, 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('..')) +# import os +# import sys +# sys.path.insert(0, os.path.join(os.path.dirname( +# os.path.dirname(os.path.abspath(__file__))), 'src')) # -- General configuration ----------------------------------------------------- @@ -33,28 +29,23 @@ # The suffix of source filenames. source_suffix = '.txt' -# The encoding of source files. -#source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = 'index' # General information about the project. -project = u'FormEncode' -copyright = u'2008-2012, Ian Bicking and Contributors' +project = 'FormEncode' +copyright = '2008-2023, Ian Bicking 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 = '1.3' -# The full version, including alpha/beta/rc tags. -release = '1.3.0dev' +from pkg_resources import get_distribution +release = get_distribution('formencode').version +version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -179,8 +170,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'FormEncode.tex', u'FormEncode Documentation', - u'Ian Bicking', 'manual'), + ('index', 'FormEncode.tex', 'FormEncode Documentation', + 'Ian Bicking', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -212,6 +203,6 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'formencode', u'FormEncode Documentation', - [u'Ian Bicking'], 1) + ('index', 'formencode', 'FormEncode Documentation', + ['Ian Bicking'], 1) ] diff --git a/docs/index.txt b/docs/index.txt index 9c9f8a6a..c9efb354 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -21,6 +21,7 @@ What's New .. toctree:: :maxdepth: 1 + whatsnew-2.0 whatsnew-1.3 whatsnew-1.2.5 whatsnew-0-to-1.2.4 diff --git a/docs/modules/htmlfill.txt b/docs/modules/htmlfill.txt index ad13455c..0cff1dd1 100644 --- a/docs/modules/htmlfill.txt +++ b/docs/modules/htmlfill.txt @@ -13,5 +13,4 @@ Module Contents .. autofunction:: escape_formatter .. autofunction:: escapenl_formatter .. autofunction:: ignore_formatter -.. autofunction:: ignore_formatter .. autoclass:: FillingParser diff --git a/docs/modules/schema.txt b/docs/modules/schema.txt index f877abcb..c78d9856 100644 --- a/docs/modules/schema.txt +++ b/docs/modules/schema.txt @@ -8,3 +8,5 @@ Module Contents .. autoclass:: Schema +.. autoclass:: SimpleFormValidator + diff --git a/docs/whatsnew-0-to-1.2.4.txt b/docs/whatsnew-0-to-1.2.4.txt index b5e5ca47..94a4d6d9 100644 --- a/docs/whatsnew-0-to-1.2.4.txt +++ b/docs/whatsnew-0-to-1.2.4.txt @@ -60,7 +60,7 @@ What's New In FormEncode 0 to 1.2.4 * Added :class:`formencode.validators.IPAddress`, validating IP addresses, from Leandro Lucarella. -* Added :meth:`formencode.api.Invalid.__unicode__` +* Added method ``Invalid.__unicode__`` * In :mod:`formencode.htmlfill` use a default encoding of utf8 when handling mixed ``str``/``unicode`` content. Also do not modify @@ -125,7 +125,7 @@ What's New In FormEncode 0 to 1.2.4 ```` -- this will cause the error to be swallowed, not shown to the user. -* Added :class:`formencode.validators.XRI` for validation i-names, +* Added ``formencode.validators.XRI`` for validation i-names, i-numbers, URLs, etc (as used in OpenID). * Look in ``/usr/share/locale`` for locale files, in addition to the diff --git a/docs/whatsnew-1.2.5.txt b/docs/whatsnew-1.2.5.txt index c4ea4d30..9b4a8104 100644 --- a/docs/whatsnew-1.2.5.txt +++ b/docs/whatsnew-1.2.5.txt @@ -33,5 +33,5 @@ Backwards Incompatibilities Documentation Enhancements -------------------------- -- Superceded news with whatsnew documents that will be archived for each +- Superseded news with whatsnew documents that will be archived for each version. Archived all news prior to 1.2.5 in :doc:`/whatsnew-0-to-1.2.4`. diff --git a/docs/whatsnew-1.3.txt b/docs/whatsnew-1.3.txt index e194b3d2..bea1951c 100644 --- a/docs/whatsnew-1.3.txt +++ b/docs/whatsnew-1.3.txt @@ -27,6 +27,4 @@ Backwards Incompatibilities - The `FancyValidator` methods `_to_python`, `_from_python`, `validate_python` and `validate_other` have been renamed to `_convert_to_python`, `_convert_from_python`, `_validate_python` and `_validate_other`, respectively. This has been done to clarify that while these methods are meant to be overridden by custom validators, they are not part of the external API. They are only helper methods that are called internally by the external methods `to_python` and `from_python`, which constitute the external API. Particularly, do not assume that `_validate_python` catches all validation errors that a call of `to_python` will catch. Please have a look at the `FancyValidator` docstring and source if you're unsure how these methods work together. For the same reason, the `CompoundValidator` method `attempt_convert` has been renamed to `_attempt_convert`. For now, the old method names will still work, but they will output deprecation warnings. -Documentation Enhancements --------------------------- - + - (1.3.1) Don't use universal wheel diff --git a/docs/whatsnew-2.0.txt b/docs/whatsnew-2.0.txt new file mode 100644 index 00000000..9a54e515 --- /dev/null +++ b/docs/whatsnew-2.0.txt @@ -0,0 +1,57 @@ +What's New In FormEncode 2.x +============================ + +This article explains the latest changes in `FormEncode` version 2.x +compared to its predecessor, `FormEncode` version 1.3. + +2.1.1 +----- + + - Add support for 3.13 + - Don't require `legacy-cgi` to be installed on 3.13 and later + (`#176 `_) + - Don't permit `FieldStorageUploadConverter` to be instantiated without + having `legacy-cgi` installed since it does not make sense + - Releases are now automated through GitHub Actions + (`#184 `_) + + +Note: This is the last version that will support Python 3.7 and 3.8 as +those are now out of support. + +2.1.0 +----- + + - Add support for Python 3.7 to 3.12, end support for older Python versions + - Context.set() now works as a context manager + - Fix binary of swedish translation + - Some internal code cleanup and modernization + +2.0.1 +----- + + - Add support for Python 3.10 + - use Pytest instead of Nose and GitHub Actions instead of Travis for tests + - Documentation updates + - Note this will be the last version to support Python 2. + The next version will be 2.1.0 to signal this change. + For compatibility with older Python versions, please use versions < 2.1. + +2.0.0 +----- + + - `FormEncode` can now run on Python 3.6 and higher without needing to run 2to3 first. + - `FormEncode` 2.0 is no longer compatible with Python 2.6 and 3.2 to 3.5. + For compatibility with older Python versions, please use versions < 1.3. + - This will be the last major version to support Python 2. + - Add strict flag to ``USPostalCode`` to raise error on postal codes that has too + many digits instead of just truncating + - Various Python 3 fixes + - Serbian latin translation + - Changed License to MIT + - Dutch, UK, Greek and South Korean postal code format fixes + - Add postal code formats for Switzerland, Cyprus, Faroe Islands, San Marino, Ukraine and Vatican City. + - Add ISODateTimeConverter validator + - Add ability to target ``htmlfill`` to particular form or ignore a form + - Fix format errors in some translations + - The version of the library can be checked using ``formencode.__version__`` diff --git a/examples/WebwareExamples/index.py b/examples/WebwareExamples/index.py index b261e6b5..4a3fdd7b 100644 --- a/examples/WebwareExamples/index.py +++ b/examples/WebwareExamples/index.py @@ -72,10 +72,10 @@ def save(self): fields = self.request().fields() try: fields = FormSchema.to_python(fields, self) - except Invalid, e: - errors = dict((k, v.encode('utf-8')) - for k, v in e.unpack_errors().iteritems()) - print "Errors:", errors + except Invalid as e: + errors = {k: v.encode('utf-8') + for k, v in e.unpack_errors().items()} + print("Errors:", errors) self.rendered_form = htmlfill.render(form_template, defaults=fields, errors=errors) self.writeHTML() @@ -83,7 +83,7 @@ def save(self): self.doAction(fields) def doAction(self, fields): - print "Fields:", fields + print("Fields:", fields) self.rendered_form = response_template % fields self.writeHTML() diff --git a/formencode/__init__.py b/formencode/__init__.py deleted file mode 100644 index de1de205..00000000 --- a/formencode/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# formencode package - -from formencode.api import ( - NoDefault, Invalid, Validator, Identity, - FancyValidator, is_empty, is_validator) -from formencode.schema import Schema -from formencode.compound import CompoundValidator, Any, All, Pipe -from formencode.foreach import ForEach -from formencode import validators -from formencode import national -from formencode.variabledecode import NestedVariables diff --git a/formencode/i18n/sr/LC_MESSAGES/FormEncode.mo b/formencode/i18n/sr/LC_MESSAGES/FormEncode.mo deleted file mode 100644 index 1c4d146e..00000000 Binary files a/formencode/i18n/sr/LC_MESSAGES/FormEncode.mo and /dev/null differ diff --git a/formencode/i18n/sv/LC_MESSAGES/FormEncode.mo b/formencode/i18n/sv/LC_MESSAGES/FormEncode.mo deleted file mode 100644 index aa132e37..00000000 Binary files a/formencode/i18n/sv/LC_MESSAGES/FormEncode.mo and /dev/null differ diff --git a/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.mo b/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.mo deleted file mode 100644 index da78e02f..00000000 Binary files a/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.mo and /dev/null differ diff --git a/formencode/tests/__init__.py b/formencode/tests/__init__.py deleted file mode 100644 index a380a390..00000000 --- a/formencode/tests/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -import sys -import os - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) - -# Make sure messages are not translated when running the tests -# (setting the environment variable here may be too late already, -# in this case you must set it manually before running the tests). -os.environ['LANGUAGE'] = 'C' - -# Enable deprecation warnings (disabled by default in Python > 2.6). -import warnings -warnings.simplefilter('default') - -import pkg_resources -pkg_resources.require('FormEncode') diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..8a0985f0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +requires = ["setuptools", "wheel", "setuptools_scm>=7"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..787b78f4 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +# content of pytest.ini +[pytest] +#addopts = --doctest-modules diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..7a52c582 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +build +wheel +flake8 +sphinx +-r requirements-test.txt diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 00000000..ac1ea171 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,2 @@ +sphinx >=7, <8 +-e . diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 00000000..4582cadb --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,5 @@ +dnspython >= 2 +pycountry +pytest +pytest-cov +wheel diff --git a/run-tests-generate-coverage-html b/run-tests-generate-coverage-html deleted file mode 100755 index a73069f1..00000000 --- a/run-tests-generate-coverage-html +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -nosetests --with-coverage --cover-package=formencode --cover-erase --cover-html --cover-html-dir=coverage \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 25332a75..7ebd9621 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,51 @@ -[nosetests] -detailed-errors = 1 +[metadata] +name = FormEncode +description = "HTML form validation, generation, and conversion package" +long_description = file: README.rst +long_description_content_type = text/x-rst +author = Ian Bicking +author_email = ianb@colorstudy.com +maintainer = Chris Lambacher +maintainer_email = chris@kateandchris.net +url = https://www.formencode.org/ +license = MIT +license_files = LICENSE.txt +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Topic :: Software Development :: Libraries :: Python Modules + +[options] +package_dir = + = src +packages = + formencode +python_requires = >=3.7 +include_package_data = False + +[options.package_data] +formencode = + javascript/*.js + i18n/**/*.mo + +[options.extras_require] +testing = + pytest + dnspython >= 2 + pycountry # Babel configuration [compile_catalog] @@ -25,6 +71,9 @@ input_file = formencode/i18n/FormEncode.pot output_dir = formencode/i18n previous = true -[wheel] -universal = 1 +[pep8] +max_line_length=88 +[flake8] +max-line-length = 88 +extend-ignore = E128,E402,E501,E731 diff --git a/setup.py b/setup.py index fdd032cc..0b62acc5 100755 --- a/setup.py +++ b/setup.py @@ -5,46 +5,6 @@ The official repo is at GitHub: https://github.com/formencode/formencode """ +from setuptools import setup -import sys -from setuptools import setup, find_packages - -version = '1.3.0a1' - -if not '2.6' <= sys.version < '3.0' and not '3.2' <= sys.version: - raise ImportError('Python version not supported') - -tests_require = ['nose', 'pycountry', - 'dnspython' if sys.version < '3.0' else 'dnspython3'] - -doctests = ['docs/htmlfill.txt', 'docs/Validator.txt', - 'formencode/tests/non_empty.txt'] - -setup(name='FormEncode', - version=version, - # requires_python='>=2.6,!=3.0,!=3.1', # PEP345 - description="HTML form validation, generation, and conversion package", - long_description=__doc__, - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Python Software Foundation License", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - author='Ian Bicking', - author_email='ianb@colorstudy.com', - url='http://formencode.org', - license='PSF', - zip_safe=False, - packages=find_packages(), - include_package_data=True, - package_data={'formencode': ['../docs/*.txt']}, - test_suite='formencode.tests', - tests_require=tests_require, - extras_require={'testing': tests_require}, - use_2to3=True, - convert_2to3_doctests=doctests, - ) +setup() diff --git a/src/formencode/__init__.py b/src/formencode/__init__.py new file mode 100644 index 00000000..fde4099a --- /dev/null +++ b/src/formencode/__init__.py @@ -0,0 +1,32 @@ +"""The formencode package""" + +try: + from importlib.metadata import version, PackageNotFoundError +except ImportError: # Python < 3.8 + from pkg_resources import get_distribution + from pkg_resources import DistributionNotFound as PackageNotFoundError + + def version(distribution_name): + return get_distribution(distribution_name).version + +from formencode.api import ( + NoDefault, Invalid, Validator, Identity, + FancyValidator, is_empty, is_validator) +from formencode.schema import Schema +from formencode.compound import CompoundValidator, Any, All, Pipe +from formencode.foreach import ForEach +from formencode import validators +from formencode import national +from formencode.variabledecode import NestedVariables + +__all__ = [ + 'NoDefault', 'Invalid', 'Validator', 'Identity', 'FancyValidator', + 'is_empty', 'is_validator', + 'Schema', 'CompoundValidator', 'Any', 'All', 'Pipe', + 'ForEach', 'validators', 'national', 'NestedVariables', + '__version__'] + +try: + __version__ = version(__name__) +except PackageNotFoundError: # package is not installed + __version__ = 'local-test' diff --git a/formencode/api.py b/src/formencode/api.py similarity index 85% rename from formencode/api.py rename to src/formencode/api.py index 4f7655cd..88c62b39 100644 --- a/formencode/api.py +++ b/src/formencode/api.py @@ -2,52 +2,43 @@ Core classes for validation. """ -from . import declarative import gettext import os import re import textwrap import warnings -try: - from pkg_resources import resource_filename -except ImportError: - resource_filename = None +from . import declarative __all__ = ['NoDefault', 'Invalid', 'Validator', 'Identity', 'FancyValidator', 'is_empty', 'is_validator'] def get_localedir(): - """Retrieve the location of locales. - - If we're built as an egg, we need to find the resource within the egg. - Otherwise, we need to look for the locales on the filesystem or in the - system message catalog. + """Retrieve the location of locales.""" + file_dir = os.path.join(os.path.dirname(__file__), 'i18n') + if not hasattr(os, 'access'): + # this happens on Google App Engine + return file_dir - """ - locale_dir = '' - # Check the egg first - if resource_filename is not None: - try: - locale_dir = resource_filename(__name__, "/i18n") - except NotImplementedError: - # resource_filename doesn't work with non-egg zip files + import importlib.resources as resources + # try to get it via the resource + pkg_name = __name__.split('.', 1)[0] + try: + resource_dir = resources.files(pkg_name) / 'i18n' + except AttributeError: # Python < 3.11 + with resources.path(pkg_name, 'i18n') as resource_dir: pass - if not hasattr(os, 'access'): - # This happens on Google App Engine - return os.path.join(os.path.dirname(__file__), 'i18n') - if os.access(locale_dir, os.R_OK | os.X_OK): - # If the resource is present in the egg, use it - return locale_dir + if os.access(resource_dir, os.R_OK | os.X_OK): + # if the resource is present, use it + return resource_dir - # Otherwise, search the filesystem - locale_dir = os.path.join(os.path.dirname(__file__), 'i18n') - if not os.access(locale_dir, os.R_OK | os.X_OK): - # Fallback on the system catalog - locale_dir = os.path.normpath('/usr/share/locale') + # otherwise, search the filesystem + if os.access(file_dir, os.R_OK | os.X_OK): + return file_dir - return locale_dir + # fallback on the system catalog + return os.path.normpath('/usr/share/locale') def set_stdtranslation(domain="FormEncode", languages=None, @@ -57,18 +48,17 @@ def set_stdtranslation(domain="FormEncode", languages=None, languages=languages, localedir=localedir, fallback=True) global _stdtrans - try: - _stdtrans = t.ugettext - except AttributeError: # Python 3 - _stdtrans = t.gettext + _stdtrans = t.gettext + set_stdtranslation() # Dummy i18n translation function, nothing is translated here. -# Instead this is actually done in api.Validator.message. +# Instead, this is actually done in api.Validator.message. # The surrounding _('string') of the strings is only for extracting # the strings automatically. -# If you run pygettext with this source comment this function out temporarily. +# If you run pygettext with this source, comment this function out temporarily. + _ = lambda s: s @@ -90,13 +80,13 @@ def inner(*args, **kwargs): return outer -class NoDefault(object): +class NoDefault: """A dummy value used for parameters with no default.""" def is_empty(value): """Check whether the given value should be considered "empty".""" - return value is None or value == '' or ( + return value is None or value == '' or value == b'' or ( isinstance(value, (list, tuple, dict)) and not value) @@ -147,18 +137,7 @@ def __init__(self, msg, % (self, self.error_list, self.error_dict)) def __str__(self): - val = self.msg - return val - - if unicode is not str: # Python 2 - - def __unicode__(self): - if isinstance(self.msg, unicode): - return self.msg - elif isinstance(self.msg, str): - return self.msg.decode('utf8') - else: - return unicode(self.msg) + return self.msg def unpack_errors(self, encode_variables=False, dict_char='.', list_char='-'): @@ -177,15 +156,15 @@ def unpack_errors(self, encode_variables=False, dict_char='.', for item in self.error_list] if self.error_dict: result = {} - for name, item in self.error_dict.iteritems(): + for name, item in self.error_dict.items(): result[name] = item if isinstance( - item, basestring) else item.unpack_errors() + item, str) else item.unpack_errors() if encode_variables: from . import variabledecode result = variabledecode.variable_encode( result, add_repetitions=False, dict_char=dict_char, list_char=list_char) - for key in result.keys(): + for key in list(result.keys()): if not result[key]: del result[key] return result @@ -194,9 +173,7 @@ def unpack_errors(self, encode_variables=False, dict_char='.', return self.msg -############################################################ -## Base Classes -############################################################ +# Base Classes class Validator(declarative.Declarative): @@ -245,8 +222,8 @@ def message(self, msgName, state, **kw): except AttributeError: try: if self.use_builtins_gettext: - import __builtin__ - trans = __builtin__._ + import builtins + trans = builtins._ else: trans = _stdtrans except AttributeError: @@ -255,8 +232,7 @@ def message(self, msgName, state, **kw): if not callable(trans): trans = _stdtrans - msg = self._messages[msgName] - msg = trans(msg, **self.gettextargs) + msg = trans(self._messages[msgName], **self.gettextargs) try: return msg % kw @@ -309,11 +285,10 @@ def _initialize_docstring(cls): This changes the class's docstring to include information about all the messages this validator uses. """ - doc = cls.__doc__ or '' - doc = [textwrap.dedent(doc).rstrip()] - messages = sorted(cls._messages.iteritems()) - doc.append('\n\n**Messages**\n\n') - for name, default in messages: + doc = [ + textwrap.dedent(cls.__doc__ or '').rstrip(), + '\n\n**Messages**\n\n'] + for name, default in sorted(cls._messages.items()): default = re.sub(r'(%\(.*?\)[rsifcx])', r'``\1``', default) doc.append('``' + name + '``:\n') doc.append(' ' + default + '\n\n') @@ -325,6 +300,7 @@ class _Identity(Validator): def __repr__(self): return 'validators.Identity' + Identity = _Identity() @@ -451,12 +427,11 @@ def __classinit__(cls, new_attrs): stacklevel=cls._inheritance_level + 2) setattr(cls, new, new_attrs[old]) elif new in new_attrs: - setattr(cls, old, deprecated(old=old, new=new)( - new_attrs[new])) + setattr(cls, old, deprecated(old=old, new=new)(new_attrs[new])) def to_python(self, value, state=None): try: - if self.strip and isinstance(value, basestring): + if self.strip and isinstance(value, str): value = value.strip() elif hasattr(value, 'mixed'): # Support Paste's MultiDict @@ -484,7 +459,7 @@ def to_python(self, value, state=None): def from_python(self, value, state=None): try: - if self.strip and isinstance(value, basestring): + if self.strip and isinstance(value, str): value = value.strip() if not self.accept_python: if self.is_empty(value): @@ -520,7 +495,7 @@ def empty_value(self, value): return None def assert_string(self, value, state): - if not isinstance(value, basestring): + if not isinstance(value, str): raise Invalid(self.message('badType', state, type=type(value), value=value), value, state) diff --git a/formencode/compound.py b/src/formencode/compound.py similarity index 89% rename from formencode/compound.py rename to src/formencode/compound.py index e01c4e00..5caf950d 100644 --- a/formencode/compound.py +++ b/src/formencode/compound.py @@ -1,16 +1,13 @@ """ Validators for applying validations in sequence. """ - -from .api import (FancyValidator, Identity, Invalid, NoDefault, Validator, - is_validator) +from .api import ( + FancyValidator, Identity, Invalid, NoDefault, Validator, is_validator) __all__ = ['CompoundValidator', 'Any', 'All', 'Pipe'] -############################################################ -## Compound Validators -############################################################ +# Compound Validators def to_python(validator, value, state): return validator.to_python(value, state) @@ -38,14 +35,14 @@ class CompoundValidator(FancyValidator): @staticmethod def __classinit__(cls, new_attrs): FancyValidator.__classinit__(cls, new_attrs) - toAdd = [] - for name, value in new_attrs.iteritems(): + to_add = [] + for name, value in new_attrs.items(): if is_validator(value) and value is not Identity: - toAdd.append((name, value)) + to_add.append((name, value)) # @@: Should we really delete too? delattr(cls, name) - toAdd.sort() - cls.validators.extend([value for _name, value in toAdd]) + to_add.sort() + cls.validators.extend([value for _name, value in to_add]) def __init__(self, *args, **kw): Validator.__init__(self, *args, **kw) @@ -133,12 +130,12 @@ def accept_iterator(self): class All(CompoundValidator): - """Check if all of the specified validators are valid. + """Check that specified validators are valid. - This class is like an 'and' operator for validators. All - validators must work, and the results are passed in turn through - all validators for conversion in the order of evaluation. All - is the same as `Pipe` but operates in the reverse order. + This class is like an 'and' operator for validators. + All validators must work, and the results are passed in turn through + all validators for conversion in the order of evaluation. + ``All`` is the same as ``Pipe`` but operates in the reverse order. The order of evaluation differs depending on if you are validating to Python or from Python as follows: @@ -147,7 +144,7 @@ class All(CompoundValidator): The validators are evaluated left to right when validating from Python. - `Pipe` is more intuitive when predominantly validating to Python. + ``Pipe`` is more intuitive when predominantly validating to Python. Examples:: @@ -200,7 +197,7 @@ def join(cls, *validators): filtering out None and trying to keep `All` validators from being nested (which isn't needed). """ - validators = filter(lambda v: v and v is not Identity, validators) + validators = [v for v in validators if v and v is not Identity] if not validators: return Identity if len(validators) == 1: diff --git a/formencode/context.py b/src/formencode/context.py similarity index 90% rename from formencode/context.py rename to src/formencode/context.py index 8bd8c72a..f1c0b68b 100644 --- a/formencode/context.py +++ b/src/formencode/context.py @@ -25,7 +25,7 @@ def do_stuff(): context object to pass information to another thread. In a single-thread environment it doesn't really matter. -Typically you will create ``Context`` instances for your application, +Typically, you will create ``Context`` instances for your application, environment, etc. These should be global module-level variables, that may be imported by any interested module; each instance is a namespace of its own. @@ -41,12 +41,12 @@ def do_stuff(): value. ``None`` is a typical value for that. Another is ``context.set_default(**vars)``, which will set only those -variables to default values. This will not effect the stack of -scopes, but will only add defaults. +variables to default values. This will not affect the stack of scopes, +but will only add defaults. -When Python 2.5 comes out, this syntax would certainly be useful:: +A context can be also used like this:: - with context(page='view'): + with context.set(page='view'): do stuff... And ``page`` will be set to ``'view'`` only inside that ``with`` block. @@ -61,7 +61,7 @@ def do_stuff(): _restore_ids = count() -class NoDefault(object): +class NoDefault: """A dummy value used for parameters with no default.""" @@ -69,7 +69,7 @@ class ContextRestoreError(Exception): """Raised when something is restored out-of-order.""" -class Context(object): +class Context: def __init__(self, default=NoDefault): self.__dict__['_local'] = threading.local() @@ -93,10 +93,10 @@ def __getattr__(self, attr): def __setattr__(self, attr, value): raise AttributeError( - "You can only write attribute on context object with the .set() method") + "You can only set attributes on context objects with the .set() method") def set(self, **kw): - state_id = _restore_ids.next() + state_id = next(_restore_ids) try: stack = self._local.stack except AttributeError: @@ -151,13 +151,19 @@ def __repr__(self): self.__class__.__name__, myid, ' '.join(varlist)) -class RestoreState(object): +class RestoreState: def __init__(self, context, state_id): self.state_id = state_id self.context = context self.restored = False + def __enter__(self): + return self.context + + def __exit__(self, _exc_type, _exc_value, _exc_tb): + self.restore() + def restore(self): if self.restored: # @@: Should this really be allowed? diff --git a/formencode/declarative.py b/src/formencode/declarative.py similarity index 92% rename from formencode/declarative.py rename to src/formencode/declarative.py index 504c44c9..550a2703 100644 --- a/formencode/declarative.py +++ b/src/formencode/declarative.py @@ -24,7 +24,7 @@ from itertools import count -class classinstancemethod(object): +class classinstancemethod: """ Acts like a class method when called from a class, like an instance method when called by an instance. The method should @@ -39,7 +39,7 @@ def __get__(self, obj, cls=None): return _methodwrapper(self.func, obj=obj, cls=cls) -class _methodwrapper(object): +class _methodwrapper: def __init__(self, func, obj, cls): self.func = func @@ -55,9 +55,9 @@ def __call__(self, *args, **kw): def __repr__(self): if self.obj is None: return ('' - % (self.cls.__name__, self.func.func_name)) + % (self.cls.__name__, self.func.__name__)) return ('' - % (self.cls.__name__, self.func.func_name, self.obj)) + % (self.cls.__name__, self.func.__name__, self.obj)) class DeclarativeMeta(type): @@ -66,11 +66,11 @@ def __new__(meta, class_name, bases, new_attrs): cls = type.__new__(meta, class_name, bases, new_attrs) for name in cls.__mutableattributes__: setattr(cls, name, copy.copy(getattr(cls, name))) - cls.declarative_count = cls.counter.next() + cls.declarative_count = next(cls.counter) if ('__classinit__' in new_attrs and not isinstance(cls.__classinit__, (staticmethod, types.FunctionType))): setattr(cls, '__classinit__', - staticmethod(cls.__classinit__.im_func)) + staticmethod(cls.__classinit__.__func__)) cls.__classinit__(cls, new_attrs) names = getattr(cls, '__singletonmethods__', None) if names: @@ -81,7 +81,7 @@ def __new__(meta, class_name, bases, new_attrs): return cls -class singletonmethod(object): +class singletonmethod: """ For Declarative subclasses, this decorator will call the method on the cls.singleton() object if called as a class method (or @@ -97,14 +97,12 @@ def __get__(self, obj, cls=None): return types.MethodType(self.func, obj) -class Declarative(object): +class Declarative(metaclass=DeclarativeMeta): __unpackargs__ = () __mutableattributes__ = () - __metaclass__ = DeclarativeMeta - __singletonmethods__ = () counter = count() @@ -143,10 +141,10 @@ def __init__(self, *args, **kw): for name in self.__mutableattributes__: if name not in kw: setattr(self, name, copy.copy(getattr(self, name))) - for name, value in kw.iteritems(): + for name, value in kw.items(): setattr(self, name, value) if 'declarative_count' not in kw: - self.declarative_count = self.counter.next() + self.declarative_count = next(self.counter) self.__initargs__(kw) def __initargs__(self, new_attrs): @@ -179,7 +177,7 @@ def __sourcerepr__(self, source, binding=None): and self.__unpackargs__[1] in vals): v = vals[self.__unpackargs__[1]] if isinstance(v, (list, int)): - args.extend(map(source.makeRepr, v)) + args.extend(list(map(source.makeRepr, v))) del v[self.__unpackargs__[1]] for name in self.__unpackargs__: if name in vals: @@ -188,7 +186,7 @@ def __sourcerepr__(self, source, binding=None): else: break args.extend('%s=%s' % (name, source.makeRepr(value)) - for (name, value) in vals.iteritems()) + for (name, value) in vals.items()) return '%s(%s)' % (self.__class__.__name__, ', '.join(args)) diff --git a/formencode/doctest_xml_compare.py b/src/formencode/doctest_xml_compare.py similarity index 93% rename from formencode/doctest_xml_compare.py rename to src/formencode/doctest_xml_compare.py index 7f3a8bf9..395d697a 100644 --- a/formencode/doctest_xml_compare.py +++ b/src/formencode/doctest_xml_compare.py @@ -1,17 +1,14 @@ - import doctest import xml.etree.ElementTree as ET -try: - XMLParseError = ET.ParseError -except AttributeError: # Python < 2.7 - from xml.parsers.expat import ExpatError as XMLParseError + +XMLParseError = ET.ParseError RealOutputChecker = doctest.OutputChecker def debug(*msg): import sys - print >> sys.stderr, ' '.join(map(str, msg)) + print(' '.join(map(str, msg)), file=sys.stderr) class HTMLOutputChecker(RealOutputChecker): @@ -69,7 +66,7 @@ def xml_compare(x1, x2, reporter=None): if reporter: reporter('Tags do not match: %s and %s' % (x1.tag, x2.tag)) return False - for name, value in x1.attrib.iteritems(): + for name, value in x1.attrib.items(): if x2.attrib.get(name) != value: if reporter: reporter('Attributes do not match: %s=%r, %s=%r' @@ -120,7 +117,7 @@ def make_xml(s): def make_string(xml): - if isinstance(xml, basestring): + if isinstance(xml, str): xml = make_xml(xml) s = ET.tostring(xml) if s == '': diff --git a/formencode/exc.py b/src/formencode/exc.py similarity index 79% rename from formencode/exc.py rename to src/formencode/exc.py index 8bc75298..3abdaf4a 100644 --- a/formencode/exc.py +++ b/src/formencode/exc.py @@ -2,7 +2,7 @@ Custom exceptions and warnings. """ -__all__ = ['FERunTimeWarning'] +__all__ = ['FERuntimeWarning'] class FERuntimeWarning(RuntimeWarning): diff --git a/formencode/fieldstorage.py b/src/formencode/fieldstorage.py similarity index 58% rename from formencode/fieldstorage.py rename to src/formencode/fieldstorage.py index a581320a..93b2ccd8 100644 --- a/formencode/fieldstorage.py +++ b/src/formencode/fieldstorage.py @@ -1,11 +1,7 @@ -## FormEncode, a Form processor -## Copyright (C) 2003, Ian Bicking """ Wrapper class for use with cgi.FieldStorage types for file uploads """ -import cgi - def convert_fieldstorage(fs): return fs if fs.filename else None diff --git a/formencode/foreach.py b/src/formencode/foreach.py similarity index 90% rename from formencode/foreach.py rename to src/formencode/foreach.py index b81e2957..96c064a1 100644 --- a/formencode/foreach.py +++ b/src/formencode/foreach.py @@ -14,13 +14,13 @@ class ForEach(CompoundValidator): For instance:: - ForEach(AsInt(), InList([1, 2, 3])) + ForEach(Int(), OneOf([1, 2, 3])) Will take a list of values and try to convert each of them to an integer, and then check if each integer is 1, 2, or 3. Using multiple arguments is equivalent to:: - ForEach(All(AsInt(), InList([1, 2, 3]))) + ForEach(All(Int(), OneOf([1, 2, 3]))) Use convert_to_list=True if you want to force the input to be a list. This will turn non-lists into one-element lists, and None @@ -83,7 +83,7 @@ def _attempt_convert(self, value, state, validate): new_list = set(new_list) return new_list else: - raise Invalid('Errors:\n%s' % '\n'.join(unicode(e) + raise Invalid('Errors:\n%s' % '\n'.join(str(e) for e in errors if e), value, state, error_list=errors) finally: if state is not None: @@ -105,15 +105,14 @@ def _attempt_convert(self, value, state, validate): def empty_value(self, value): return [] - class _IfMissing(object): + class _IfMissing: def __get__(self, obj, cls=None): if obj is None: return [] - elif obj._if_missing is ForEach._if_missing: + if obj._if_missing is ForEach._if_missing: return [] - else: - return obj._if_missing + return obj._if_missing def __set__(self, obj, value): obj._if_missing = value @@ -125,7 +124,7 @@ def __delete__(self, obj): del _IfMissing def _convert_to_list(self, value): - if isinstance(value, basestring): + if isinstance(value, str): return [value] elif value is None: return [] @@ -135,6 +134,5 @@ def _convert_to_list(self, value): for _n in value: break return value - ## @@: Should this catch any other errors?: - except TypeError: + except TypeError: # @@: Should this catch any other errors? return [value] diff --git a/formencode/htmlfill.py b/src/formencode/htmlfill.py similarity index 90% rename from formencode/htmlfill.py rename to src/formencode/htmlfill.py index c06ddc9b..a9fd12d9 100644 --- a/formencode/htmlfill.py +++ b/src/formencode/htmlfill.py @@ -17,7 +17,9 @@ def render(form, defaults=None, errors=None, use_all_keys=False, text_as_default=False, checkbox_checked_if_present=False, listener=None, encoding=None, error_class='error', prefix_error=True, - force_defaults=True, skip_passwords=False): + force_defaults=True, skip_passwords=False, + data_formencode_form=None, data_formencode_ignore=None, + ): """ Render the ``form`` (which should be a string) given the ``defaults`` and ``errors``. Defaults are the values that go in the input fields @@ -66,8 +68,8 @@ def render(form, defaults=None, errors=None, use_all_keys=False, ``listener`` can be an object that watches fields pass; the only one currently is in ``htmlfill_schemabuilder.SchemaBuilder`` - ``encoding`` specifies an encoding to assume when mixing str and - unicode text in the template. + ``encoding`` specifies an encoding to assume when mixing bytes and + str text in the template. ``prefix_error`` specifies if the HTML created by auto_error_formatter is put before the input control (default) or after the control. @@ -83,6 +85,19 @@ def render(form, defaults=None, errors=None, use_all_keys=False, rendering form-content. If disabled the password fields will not be filled with anything, which is useful when you don't want to return a user's password in plain-text source. + + ``data_formencode_form`` if a string is passed in (default `None`) only + fields with the html attribute `data-formencode-form` that matches this + string will be processed. For example: if a HTML fragment has two forms they + can be differentiated to Formencode by decorating the input elements with + attributes such as `data-formencode-form="a"` or `data-formencode-form="b"`, + then instructing `render()` to only process the "a" or "b" fields. + + ``data_formencode_ignore`` if True (default `None`) fields with the html + attribute `data-formencode-ignore` will not be processed. This attribute + need only be present in the tag: `data-formencode-ignore="1"`, + `data-formencode-ignore=""` and `data-formencode-ignore` without a value are + all valid signifiers. """ if defaults is None: defaults = {} @@ -101,13 +116,15 @@ def render(form, defaults=None, errors=None, use_all_keys=False, error_class=error_class, force_defaults=force_defaults, skip_passwords=skip_passwords, + data_formencode_form=data_formencode_form, + data_formencode_ignore=data_formencode_ignore, ) p.feed(form) p.close() return p.text() -class htmlliteral(object): +class htmlliteral: def __init__(self, html, text=None): if text is None: @@ -189,7 +206,7 @@ class FillingParser(RewritingParser): ... ... ''') >>> parser.close() - >>> print parser.text() # doctest: +NORMALIZE_WHITESPACE + >>> print (parser.text()) # doctest: +NORMALIZE_WHITESPACE ', {}, {}) == '' % rarr) assert (htmlfill.render('', {}, {}) == '') assert (htmlfill.render('', {}, {}) == - u'') + '') def test_xhtml(): @@ -180,7 +179,7 @@ def test_checkbox(): def test_unicode(): - assert (htmlfill.render(u'', + assert (htmlfill.render('', dict(tags=[])) == '') @@ -455,10 +454,10 @@ def test_error_class_textarea(): def test_mix_str_and_unicode(): html = '' - uhtml = unicode(html) + uhtml = str(html) cheese = dict(cheese='Käse') - ucheese = dict(cheese=u'Käse') - expected = u'' + ucheese = dict(cheese='Käse') + expected = '' rendered = htmlfill.render(html, defaults=cheese, encoding='utf-8') assert expected == rendered rendered = htmlfill.render(html, defaults=ucheese, encoding='utf-8') diff --git a/tests/test_htmlfill_control.py b/tests/test_htmlfill_control.py new file mode 100644 index 00000000..e6959594 --- /dev/null +++ b/tests/test_htmlfill_control.py @@ -0,0 +1,177 @@ +from formencode import htmlfill + + +# ============================================================================== + + +def test_defaults_legacy(): + html = """ + + + +""" + expected_html = """ + + + +""" + rendered_html = htmlfill.render(html, defaults={"foo": "bang"}, + force_defaults=True) + assert expected_html == rendered_html + + +def test_defaults_attr_ignore(): + html = """ + + + + + +""" + expected_html = """ + + + + + +""" + rendered_html = htmlfill.render(html, defaults={"foo": "bang"}, + force_defaults=True, + data_formencode_ignore=True) + assert expected_html == rendered_html + + +def test_defaults_attr_form(): + html = """ + + + +""" + expected_html = """ + + + +""" + rendered_html = htmlfill.render(html, defaults={"foo": "bang"}, + force_defaults=True, + data_formencode_form="b",) + assert expected_html == rendered_html + + +# ============================================================================== + +def test_error_legacy(): + html = """ + + + +""" + expected_html = """ + +bang
+ + + +""" + rendered_html = htmlfill.render(html, errors={"foo": "bang"}, + prefix_error=True) + assert expected_html == rendered_html + + +def test_error_attr_ignore(): + html = """ + + + +""" + expected_html = """ + + +bang
+ + +""" + rendered_html = htmlfill.render(html, errors={"foo": "bang"}, + force_defaults=True, + data_formencode_form="b",) + assert expected_html == rendered_html + + +def test_error_attr_form(): + html = """ + + + +""" + expected_html = """ + + +bang
+ + +""" + rendered_html = htmlfill.render(html, errors={"foo": "bang"}, + force_defaults=True, + data_formencode_form="b",) + assert expected_html == rendered_html + + +def test_error_attr_form_alt(): + """note that formencode doesn't keep an indent on the replacement + mixes concepts + note a few things: + 1. we expect a leading "" block, because we are ignoring that tag + 1. we expect the leading "" block to not have an initial newline (\n) + """ + html = """ +
+ + +
+
+ + +
+
+ + + +
+""" + expected_html = """ +orange
+ +
+ + +
+
+ + +
+
+ + +bang
+ + +
+""" + rendered_html = htmlfill.render(html, defaults={"bar": "bang"}, + errors={"foo": "bang", "apple": "orange"}, + force_defaults=True, + data_formencode_form="c", + data_formencode_ignore=True, + ) + assert expected_html == rendered_html + + +if __name__ == '__main__': + test_defaults_legacy() + test_defaults_attr_ignore() + test_defaults_attr_form() + + test_error_legacy() + test_error_attr_ignore() + test_error_attr_form() + test_error_attr_form_alt() diff --git a/formencode/tests/test_htmlgen.py b/tests/test_htmlgen.py similarity index 86% rename from formencode/tests/test_htmlgen.py rename to tests/test_htmlgen.py index cfc07198..0af2421b 100644 --- a/formencode/tests/test_htmlgen.py +++ b/tests/test_htmlgen.py @@ -1,11 +1,9 @@ - import doctest from formencode.htmlgen import html # A test value that can't be encoded as ascii: -uni_value = u'\xff' -str_value = uni_value if str is unicode else uni_value.encode('utf-8') +uni_value = '\xff' def test_basic(): @@ -37,12 +35,12 @@ def test_unicode(): assert False, ( "We need something that can't be ASCII-encoded: %r (%r)" % (uni_value, uni_value.encode('ascii'))) - assert unicode(html.b(uni_value)) == u'%s' % uni_value + assert str(html.b(uni_value)) == '%s' % uni_value def test_quote(): assert html.quote('!') == '<hey>!' - assert html.quote(uni_value) == str_value + assert html.quote(uni_value) == uni_value assert html.quote(None) == '' assert html.str(None) == '' assert str(html.b('')) == '<hey>' @@ -58,7 +56,7 @@ def strip(s): return s assert strip(html.comment('test')) == '' - assert strip(html.comment(uni_value)) == '' % str_value + assert strip(html.comment(uni_value)) == '' % uni_value assert strip(html.comment('test')('this')) == '' @@ -76,10 +74,10 @@ def test_namespace(): if __name__ == '__main__': # It's like a super-mini py.test... - for name, value in globals().iteritems(): + for name, value in globals().items(): if name.startswith('test'): - print name + print(name) value() from formencode import htmlgen doctest.testmod(htmlgen) - print 'doctest' + print('doctest') diff --git a/formencode/tests/test_htmlrename.py b/tests/test_htmlrename.py similarity index 100% rename from formencode/tests/test_htmlrename.py rename to tests/test_htmlrename.py diff --git a/formencode/tests/test_i18n.py b/tests/test_i18n.py similarity index 51% rename from formencode/tests/test_i18n.py rename to tests/test_i18n.py index a6534d0a..77ad988a 100644 --- a/formencode/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import formencode ne = formencode.validators.NotEmpty() @@ -8,15 +6,15 @@ def _test_builtins(func): def dummy(s): return "builtins dummy" - import __builtin__ - __builtin__._ = dummy + import builtins + builtins._ = dummy try: ne.to_python("") except formencode.api.Invalid as e: func(e) - del __builtin__._ + del builtins._ def test_builtins(): @@ -35,7 +33,7 @@ def withoutbuiltins(e): def test_state(): - class st(object): + class st: def _(self, s): return "state dummy" @@ -52,90 +50,90 @@ def _test_lang(language, notemptytext): try: ne.to_python("") except formencode.api.Invalid as e: - assert unicode(e) == notemptytext + assert str(e) == notemptytext formencode.api.set_stdtranslation() # set back to defaults def test_de(): - _test_lang("de", u"Bitte einen Wert eingeben") + _test_lang("de", "Bitte einen Wert eingeben") def test_es(): - _test_lang("es", u"Por favor introduzca un valor") + _test_lang("es", "Por favor introduzca un valor") def test_pt_BR(): - _test_lang("pt_BR", u"Por favor digite um valor") + _test_lang("pt_BR", "Por favor digite um valor") def test_zh_TW(): - _test_lang("zh_TW", u"請輸入一個值") + _test_lang("zh_TW", "請輸入值") def test_sk(): - _test_lang("sk", u"Zadajte hodnotu, prosím") + _test_lang("sk", "Zadajte hodnotu, prosím") def test_ru(): - _test_lang("ru", u"Необходимо ввести значение") + _test_lang("ru", "Необходимо ввести значение") def test_sl(): - _test_lang("sl", u"Prosim, izpolnite polje") + _test_lang("sl", "Prosim, izpolnite polje") def test_pt_PT(): - _test_lang("pt_PT", u"Por favor insira um valor") + _test_lang("pt_PT", "Por favor insira um valor") def test_fr(): - _test_lang("fr", u"Saisissez une valeur") + _test_lang("fr", "Saisissez une valeur") def test_nl(): - _test_lang("nl", u"Voer een waarde in") + _test_lang("nl", "Voer een waarde in") def test_pl(): - _test_lang("pl", u"Proszę podać wartość") + _test_lang("pl", "Proszę podać wartość") def test_el(): - _test_lang("el", u"Παρακαλούμε εισάγετε μια τιμή") + _test_lang("el", "Παρακαλούμε εισάγετε μια τιμή") def test_zh_CN(): - _test_lang("zh_CN", u"请输入一个值") + _test_lang("zh_CN", "请输入一个值") def test_cs(): - _test_lang("cs", u"Prosím zadejte hodnotu") + _test_lang("cs", "Prosím zadejte hodnotu") def test_fi(): - _test_lang("fi", u"Anna arvo") + _test_lang("fi", "Anna arvo") def test_nb_NO(): - _test_lang("nb_NO", u"Venligst fyll inn en verdi") + _test_lang("nb_NO", "Venligst fyll inn en verdi") def test_it(): - _test_lang("it", u"Inserire un dato") + _test_lang("it", "Inserire un dato") def test_et(): - _test_lang("et", u"Palun sisestada väärtus") + _test_lang("et", "Palun sisestada väärtus") def test_lt(): - _test_lang("lt", u"Prašome įvesti reikšmę") + _test_lang("lt", "Prašome įvesti reikšmę") def test_ja(): - _test_lang("ja", u"入力してください") + _test_lang("ja", "入力してください") def test_tr(): - _test_lang("tr", u"Lütfen bir değer giriniz") + _test_lang("tr", "Lütfen bir değer giriniz") diff --git a/formencode/tests/test_schema.py b/tests/test_schema.py similarity index 91% rename from formencode/tests/test_schema.py rename to tests/test_schema.py index 49dc3a76..4f57bb38 100644 --- a/formencode/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,11 +1,13 @@ import unittest -from urlparse import parse_qsl +from urllib.parse import parse_qsl from formencode import Invalid, Validator, compound, foreach, validators from formencode.schema import Schema, merge_dicts, SimpleFormValidator from formencode.variabledecode import NestedVariables +import pytest + def _notranslation(s): return s @@ -14,14 +16,14 @@ def _notranslation(s): def setup_module(module): """Disable i18n translation""" - import __builtin__ - __builtin__._ = _notranslation + import builtins + builtins._ = _notranslation def teardown_module(module): """Remove translation function""" - import __builtin__ - del __builtin__._ + import builtins + del builtins._ def d(**kw): @@ -42,23 +44,22 @@ def cgi_parse(qs): return d -class DecodeCase(object): +class DecodeCase: error_expected = False def __init__(self, schema, input, **output): - self.raw_input = input - self.schema = schema if isinstance(input, str): input = cgi_parse(input) self.input = input + self.schema = schema self.output = output all_cases.append(self) def test(self): - print 'input', repr(self.input) + print('input', repr(self.input)) actual = self.schema.to_python(self.input) - print 'output', repr(actual) + print('output', repr(actual)) assert actual == self.output @@ -72,9 +73,9 @@ def __init__(self, *args, **kw): self.output = self.output['text'] def test(self): - print repr(self.raw_input) + print(repr(self.input)) try: - print repr(self.schema.to_python(self.input)) + print(repr(self.schema.to_python(self.input))) except Invalid as e: actual = e.unpack_errors() assert actual == self.output @@ -144,10 +145,14 @@ class schema(Schema): text="The input field 'whatever' was not expected.") -def test_this(): - +def collect_cases(): for case in all_cases: - yield (case.test,) + yield case.test + + +@pytest.mark.parametrize('func', list(collect_cases())) +def test_this(func): + func() def test_merge(): @@ -170,8 +175,9 @@ class ChainedTest(Schema): b = validators.String() b_confirm = validators.String() - chained_validators = [validators.FieldsMatch('a', 'a_confirm'), - validators.FieldsMatch('b', 'b_confirm')] + chained_validators = [ + validators.FieldsMatch('a', 'a_confirm'), + validators.FieldsMatch('b', 'b_confirm')] def test_multiple_chained_validators_errors(): @@ -204,7 +210,7 @@ def f(value_dict, state, validator): assert f.__doc__ == g.__doc__, "Docstrings don't match!" -class State(object): +class State: pass @@ -254,16 +260,15 @@ def from_python(self, value, state): assert state.key == old_key, "key not restored" -class TestAtLeastOneCheckboxIsChecked(object): +class TestAtLeastOneCheckboxIsChecked(unittest.TestCase): """Tests to address SourceForge bug #1777245 The reporter is trying to enforce agreement to a Terms of Service agreement, with failure to check the 'I agree' checkbox handled as a validation failure. The tests below illustrate a working approach. - """ - def setup(self): + def setUp(self): self.not_empty_messages = {'missing': 'a missing value message'} class CheckForCheckboxSchema(Schema): @@ -274,7 +279,7 @@ class CheckForCheckboxSchema(Schema): def test_Schema_with_input_present(self): # result = self.schema.to_python({'agree': 'yes'}) - assert result['agree'] is True + self.assertTrue(result['agree']) def test_Schema_with_input_missing(self): # @@ -282,10 +287,10 @@ def test_Schema_with_input_missing(self): self.schema.to_python({}) except Invalid as exc: error_message = exc.error_dict['agree'].msg - assert self.not_empty_messages['missing'] == error_message, \ - error_message + self.assertEqual(self.not_empty_messages['missing'], error_message, + error_message) else: - assert False, 'missing input not detected' + self.fail('missing input not detected') class TestStrictSchemaWithMultipleEqualInputFields(unittest.TestCase): diff --git a/formencode/tests/test_subclassing.py b/tests/test_subclassing.py similarity index 90% rename from formencode/tests/test_subclassing.py rename to tests/test_subclassing.py index 1a908d42..ec6c2cbd 100644 --- a/formencode/tests/test_subclassing.py +++ b/tests/test_subclassing.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import unittest from formencode.api import is_validator, FancyValidator, Invalid @@ -49,23 +47,20 @@ def test_to_python(self): try: cv.to_python('1') except Invalid as e: - self.assertTrue( - 'one is invalid' in str(e), e) + self.assertIn('one is invalid', str(e), e) else: self.fail("one should be invalid") self.assertEqual(cv.to_python('2'), 2) try: cv.to_python('3') except Invalid as e: - self.assertTrue( - 'three is invalid' in str(e), e) + self.assertIn('three is invalid', str(e), e) else: self.fail("three should be invalid") try: cv.to_python('4') except Invalid as e: - self.assertTrue( - 'four is invalid' in str(e), e) + self.assertIn('four is invalid', str(e), e) else: self.fail("four should be invalid") self.assertEqual(cv.to_python('5'), 5) @@ -77,8 +72,7 @@ def test_from_python(self): try: cv.from_python(2) except Invalid as e: - self.assertTrue( - 'two is invalid' in str(e), e) + self.assertIn('two is invalid', str(e), e) else: self.fail("two should be invalid") self.assertEqual(cv.from_python(3), '3') @@ -92,22 +86,19 @@ def test_from_python_no_accept(self): try: cv.from_python(2) except Invalid as e: - self.assertTrue( - 'two is invalid' in str(e), e) + self.assertIn('two is invalid', str(e), e) else: self.fail("two should be invalid") try: cv.from_python(3) except Invalid as e: - self.assertTrue( - 'three is invalid' in str(e), e) + self.assertIn('three is invalid', str(e), e) else: self.fail("three should be invalid") try: cv.from_python(4) except Invalid as e: - self.assertTrue( - 'four is invalid' in str(e), e) + self.assertIn('four is invalid', str(e), e) else: self.fail("four should be invalid") self.assertEqual(cv.from_python(5), '5') @@ -143,8 +134,7 @@ def test_to_python(self): try: nov.to_python('1') except Invalid as e: - self.assertTrue( - 'must not be 1' in str(e), e) + self.assertIn('must not be 1', str(e), e) else: self.fail("1 should be invalid") self.assertEqual(nov.to_python('2'), 2) @@ -158,8 +148,7 @@ def test_to_python_number(self): try: nov.to_python('42') except Invalid as e: - self.assertTrue( - 'must not be 42' in str(e), e) + self.assertIn('must not be 42', str(e), e) else: self.fail("42 should be invalid") @@ -174,8 +163,7 @@ def test_to_python_range(self): try: nov.to_python('42') except Invalid as e: - self.assertTrue( - 'must not be 42' in str(e), e) + self.assertIn('must not be 42', str(e), e) else: self.fail("42 should be invalid") self.assertEqual(nov.to_python('43'), 43) @@ -243,8 +231,7 @@ def test_to_python(self): try: cav.to_python('4') except Invalid as e: - self.assertTrue( - 'must not be 4' in str(e), e) + self.assertIn('must not be 4', str(e), e) else: self.fail("4 should be invalid") self.assertEqual(cav.to_python('5'), 5) diff --git a/formencode/tests/test_subclassing_old.py b/tests/test_subclassing_old.py similarity index 73% rename from formencode/tests/test_subclassing_old.py rename to tests/test_subclassing_old.py index c34a87f0..f47bd0de 100644 --- a/formencode/tests/test_subclassing_old.py +++ b/tests/test_subclassing_old.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import unittest import warnings @@ -7,7 +5,6 @@ from formencode.compound import CompoundValidator, All from formencode.validators import Int - with warnings.catch_warnings(record=True) as custom_warnings: warnings.simplefilter('default') @@ -52,7 +49,7 @@ def test_1_warnings(self): output = '\n'.join(map(str, custom_warnings)) for old, new in deprecated: msg = '%s is deprecated; use %s instead' % (old, new) - self.assertTrue(msg in output, output or 'no warnings') + self.assertIn(msg, output, output or 'no warnings') def test_is_validator(self): self.assertTrue(is_validator(DeprecatedCustomValidator)) @@ -64,23 +61,20 @@ def test_to_python(self): try: cv.to_python('1') except Invalid as e: - self.assertTrue( - 'one is invalid' in str(e), e) + self.assertIn('one is invalid', str(e), e) else: self.fail("one should be invalid") self.assertEqual(cv.to_python('2'), 2) try: cv.to_python('3') except Invalid as e: - self.assertTrue( - 'three is invalid' in str(e), e) + self.assertIn('three is invalid', str(e), e) else: self.fail("three should be invalid") try: cv.to_python('4') except Invalid as e: - self.assertTrue( - 'four is invalid' in str(e), e) + self.assertIn('four is invalid', str(e), e) else: self.fail("four should be invalid") self.assertEqual(cv.to_python('5'), 5) @@ -92,8 +86,7 @@ def test_from_python(self): try: cv.from_python(2) except Invalid as e: - self.assertTrue( - 'two is invalid' in str(e), e) + self.assertIn('two is invalid', str(e), e) else: self.fail("two should be invalid") self.assertEqual(cv.from_python(3), '3') @@ -107,22 +100,19 @@ def test_from_python_no_accept(self): try: cv.from_python(2) except Invalid as e: - self.assertTrue( - 'two is invalid' in str(e), e) + self.assertIn('two is invalid', str(e), e) else: self.fail("two should be invalid") try: cv.from_python(3) except Invalid as e: - self.assertTrue( - 'three is invalid' in str(e), e) + self.assertIn('three is invalid', str(e), e) else: self.fail("three should be invalid") try: cv.from_python(4) except Invalid as e: - self.assertTrue( - 'four is invalid' in str(e), e) + self.assertIn('four is invalid', str(e), e) else: self.fail("four should be invalid") self.assertEqual(cv.from_python(5), '5') @@ -158,7 +148,7 @@ def test_1_warnings(self): # must run first for output in runtime_warnings, not_one_warnings: output = '\n'.join(map(str, output)) msg = '_to_python is deprecated; use _convert_to_python instead' - self.assertTrue(msg in output, output or 'no warnings') + self.assertIn(msg, output, output or 'no warnings') def test_is_validator(self): self.assertTrue(is_validator(DeprecatedNotOneValidator)) @@ -167,48 +157,48 @@ def test_is_validator(self): def test_to_python(self): nov = DeprecatedNotOneValidator() - self.assertEqual(nov.to_python('0'), 0) - try: - nov.to_python('1') - except Invalid as e: - self.assertTrue( - 'must not be 1' in str(e), e) - else: - self.fail("1 should be invalid") - self.assertEqual(nov.to_python('2'), 2) - self.assertEqual(nov.to_python('42'), 42) + with warnings.catch_warnings(record=True): + self.assertEqual(nov.to_python('0'), 0) + try: + nov.to_python('1') + except Invalid as e: + self.assertIn('must not be 1', str(e), e) + else: + self.fail("1 should be invalid") + self.assertEqual(nov.to_python('2'), 2) + self.assertEqual(nov.to_python('42'), 42) def test_to_python_number(self): nov = DeprecatedNotOneValidator(number=42) - self.assertEqual(nov.to_python('0'), 0) - self.assertEqual(nov.to_python('1'), 1) - self.assertEqual(nov.to_python('2'), 2) - try: - nov.to_python('42') - except Invalid as e: - self.assertTrue( - 'must not be 42' in str(e), e) - else: - self.fail("42 should be invalid") + with warnings.catch_warnings(record=True): + self.assertEqual(nov.to_python('0'), 0) + self.assertEqual(nov.to_python('1'), 1) + self.assertEqual(nov.to_python('2'), 2) + try: + nov.to_python('42') + except Invalid as e: + self.assertIn('must not be 42', str(e), e) + else: + self.fail("42 should be invalid") def test_to_python_range(self): nov = DeprecatedNotOneValidator(min=40, max=49, number=42) - self.assertRaises(Invalid, nov.to_python, '0') - self.assertRaises(Invalid, nov.to_python, '1') - self.assertRaises(Invalid, nov.to_python, '2') - self.assertRaises(Invalid, nov.to_python, '39') - self.assertEqual(nov.to_python('40'), 40) - self.assertEqual(nov.to_python('41'), 41) - try: - nov.to_python('42') - except Invalid as e: - self.assertTrue( - 'must not be 42' in str(e), e) - else: - self.fail("42 should be invalid") - self.assertEqual(nov.to_python('43'), 43) - self.assertEqual(nov.to_python('49'), 49) - self.assertRaises(Invalid, nov.to_python, '50') + with warnings.catch_warnings(record=True): + self.assertRaises(Invalid, nov.to_python, '0') + self.assertRaises(Invalid, nov.to_python, '1') + self.assertRaises(Invalid, nov.to_python, '2') + self.assertRaises(Invalid, nov.to_python, '39') + self.assertEqual(nov.to_python('40'), 40) + self.assertEqual(nov.to_python('41'), 41) + try: + nov.to_python('42') + except Invalid as e: + self.assertIn('must not be 42', str(e), e) + else: + self.fail("42 should be invalid") + self.assertEqual(nov.to_python('43'), 43) + self.assertEqual(nov.to_python('49'), 49) + self.assertRaises(Invalid, nov.to_python, '50') with warnings.catch_warnings(record=True) as custom_compound_warnings: @@ -230,14 +220,14 @@ def setUp(self): def test_1_warnings(self): output = '\n'.join(map(str, custom_compound_warnings)) msg = 'attempt_convert is deprecated; use _attempt_convert instead' - self.assertTrue(msg in output, output or 'no warnings') + self.assertIn(msg, output, output or 'no warnings') def test_is_validator(self): self.assertTrue(is_validator(DeprecatedCustomCompoundValidator)) self.assertTrue(is_validator(self.validator)) def test_to_python(self): - with warnings.catch_warnings(record=True) as _ignore: + with warnings.catch_warnings(record=True): ccv = self.validator self.assertEqual(ccv.to_python('2'), 2) self.assertEqual(ccv.to_python('4'), 4) @@ -278,7 +268,7 @@ def test_1_warnings(self): # must run first for output in runtime_warnings, all_and_not_one_warnings: output = '\n'.join(map(str, output)) msg = 'attempt_convert is deprecated; use _attempt_convert instead' - self.assertTrue(msg in output, output or 'no warnings') + self.assertIn(msg, output, output or 'no warnings') def test_is_validator(self): self.assertTrue(is_validator(DeprecatedAllAndNotOneValidator)) @@ -286,16 +276,16 @@ def test_is_validator(self): def test_to_python(self): cav = self.validator - self.assertRaises(Invalid, cav.to_python, '1') - self.assertRaises(Invalid, cav.to_python, '2') - self.assertEqual(cav.to_python('3'), 3) - try: - cav.to_python('4') - except Invalid as e: - self.assertTrue( - 'must not be 4' in str(e), e) - else: - self.fail("4 should be invalid") - self.assertEqual(cav.to_python('5'), 5) - self.assertRaises(Invalid, cav.to_python, '6') - self.assertRaises(Invalid, cav.to_python, '7') + with warnings.catch_warnings(record=True): + self.assertRaises(Invalid, cav.to_python, '1') + self.assertRaises(Invalid, cav.to_python, '2') + self.assertEqual(cav.to_python('3'), 3) + try: + cav.to_python('4') + except Invalid as e: + self.assertIn('must not be 4', str(e), e) + else: + self.fail("4 should be invalid") + self.assertEqual(cav.to_python('5'), 5) + self.assertRaises(Invalid, cav.to_python, '6') + self.assertRaises(Invalid, cav.to_python, '7') diff --git a/formencode/tests/test_validators.py b/tests/test_validators.py similarity index 81% rename from formencode/tests/test_validators.py rename to tests/test_validators.py index 29ce97c0..5db4e085 100644 --- a/formencode/tests/test_validators.py +++ b/tests/test_validators.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- - import datetime import unittest -from nose.plugins.skip import SkipTest +import pytest from formencode import validators from formencode.validators import Invalid @@ -15,15 +13,13 @@ def validate(validator, value): try: return validator.to_python(value) - return None except Invalid as e: return e.unpack_errors() def validate_from(validator, value): try: - validator.from_python(value) - return None + return validator.from_python(value) except Invalid as e: return e.unpack_errors() @@ -33,13 +29,13 @@ class TestValidators(unittest.TestCase): def testHelp(self): from pydoc import text, plain s = plain(text.document(validators)) - self.assertTrue('Validator/Converters for use with FormEncode.' in s) - self.assertTrue('class Bool' in s) - self.assertTrue('Always Valid, returns True or False' in s) - self.assertTrue('class Email' in s) - self.assertTrue('Validate an email address.' in s) - self.assertTrue('class FieldsMatch' in s) - self.assertTrue('Tests that the given fields match' in s) + self.assertIn('Validator/Converters for use with FormEncode.', s) + self.assertIn('class Bool', s) + self.assertIn('Always Valid, returns True or False', s) + self.assertIn('class Email', s) + self.assertIn('Validate an email address.', s) + self.assertIn('class FieldsMatch', s) + self.assertIn('Tests that the given fields match', s) class TestByteStringValidator(unittest.TestCase): @@ -49,13 +45,12 @@ def setUp(self): self.messages = self.validator.message def test_alias(self): - if str is not unicode: # Python 2 - self.assertTrue(self.validator is validators.String) + self.assertIsNot(self.validator, validators.String) def test_docstring(self): doc = self.validator.__doc__ - self.assertTrue( - 'Enter a value not more than ``%(max)i`` characters long' in doc) + self.assertIn( + 'Enter a value not more than ``%(max)i`` characters long', doc) def test_sv_min(self): sv = self.validator(min=2, accept_python=False) @@ -87,56 +82,55 @@ def setUp(self): self.validator = validators.UnicodeString def test_alias(self): - if str is unicode: # Python 3 - self.assertTrue(self.validator is validators.String) + self.assertIs(self.validator, validators.String) def test_docstring(self): doc = self.validator.__doc__ - self.assertTrue('Invalid data or incorrect encoding' in doc) + self.assertIn('Invalid data or incorrect encoding', doc) def test_unicode(self): un = self.validator() - self.assertEqual(un.to_python(12), u'12') - self.assertTrue(type(un.to_python(12)) is unicode) - self.assertEqual(un.from_python(12), u'12'.encode('ascii')) - self.assertTrue(type(un.from_python(12)) is bytes) + self.assertEqual(un.to_python(12), '12') + self.assertIs(type(un.to_python(12)), str) + self.assertEqual(un.from_python(12), '12'.encode('ascii')) + self.assertIs(type(un.from_python(12)), bytes) def test_unicode_encoding(self): uv = self.validator() - us = u'käse' + us = 'käse' u7s, u8s = us.encode('utf-7'), us.encode('utf-8') self.assertEqual(uv.to_python(u8s), us) - self.assertTrue(type(uv.to_python(u8s)) is unicode) + self.assertIs(type(uv.to_python(u8s)), str) self.assertEqual(uv.from_python(us), u8s) - self.assertTrue(type(uv.from_python(us)) is bytes) + self.assertIs(type(uv.from_python(us)), bytes) uv = self.validator(encoding='utf-7') self.assertEqual(uv.to_python(u7s), us) - self.assertTrue(type(uv.to_python(u7s)) is unicode) + self.assertIs(type(uv.to_python(u7s)), str) self.assertEqual(uv.from_python(us), u7s) - self.assertTrue(type(uv.from_python(us)) is bytes) + self.assertIs(type(uv.from_python(us)), bytes) uv = self.validator(inputEncoding='utf-7') self.assertEqual(uv.to_python(u7s), us) - self.assertTrue(type(uv.to_python(u7s)) is unicode) + self.assertIs(type(uv.to_python(u7s)), str) uv = self.validator(outputEncoding='utf-7') self.assertEqual(uv.from_python(us), u7s) - self.assertTrue(type(uv.from_python(us)) is bytes) + self.assertIs(type(uv.from_python(us)), bytes) uv = self.validator(inputEncoding=None) self.assertEqual(uv.to_python(us), us) - self.assertTrue(type(uv.to_python(us)) is unicode) + self.assertIs(type(uv.to_python(us)), str) self.assertEqual(uv.from_python(us), u8s) - self.assertTrue(type(uv.from_python(us)) is bytes) + self.assertIs(type(uv.from_python(us)), bytes) uv = self.validator(outputEncoding=None) self.assertEqual(uv.to_python(u8s), us) - self.assertTrue(type(uv.to_python(u8s)) is unicode) + self.assertIs(type(uv.to_python(u8s)), str) self.assertEqual(uv.from_python(us), us) - self.assertTrue(type(uv.from_python(us)) is unicode) + self.assertIs(type(uv.from_python(us)), str) def test_unicode_empty(self): iv = self.validator() - for value in [None, "", u""]: + for value in [None, b"", ""]: result = iv.to_python(value) - self.assertEqual(result, u"") - self.assertTrue(isinstance(result, unicode)) + self.assertEqual(result, "") + self.assertIsInstance(result, str) class TestIntValidator(unittest.TestCase): @@ -147,7 +141,7 @@ def setUp(self): def test_docstring(self): doc = self.validator.__doc__ - self.assertTrue('Please enter an integer value' in doc) + self.assertIn('Please enter an integer value', doc) def test_int_min(self): iv = self.validator(min=5) @@ -163,8 +157,8 @@ def test_int_max(self): def test_int_minmax_optional(self): iv = self.validator(min=5, max=10, if_empty=None) - self.assertTrue(iv.to_python("") is None) - self.assertTrue(iv.to_python(None) is None) + self.assertIsNone(iv.to_python("")) + self.assertIsNone(iv.to_python(None)) self.assertEqual(iv.to_python('7'), 7) self.assertEqual(validate(iv, "1"), self.messages('tooLow', None, min=5)) @@ -192,7 +186,7 @@ def test_inf(self): try: inf = float('infinity') except ValueError: - raise SkipTest + pytest.skip('skipping incompatible float test') self.assertEqual(self.validator.to_python('infinity'), inf) @@ -206,15 +200,15 @@ def test_bad_dates(self): try: dc.to_python('20/12/150') except Invalid as e: - self.assertTrue( - 'Please enter a four-digit year after 1899' in str(e)) + self.assertIn( + 'Please enter a four-digit year after 1899', str(e)) else: self.fail('Date should be invalid') try: dc.to_python('oh/happy/day') except Invalid as e: - self.assertTrue( - 'Please enter the date in the form DD/MM/YYYY' in str(e)) + self.assertIn( + 'Please enter the date in the form DD/MM/YYYY', str(e)) else: self.fail('Date should be invalid') @@ -231,17 +225,16 @@ def test_month_style_alias(self): dc = self.validator(month_style=style) self.assertEqual(dc.month_style, 'ymd') try: - dc = self.validator(month_style='Klingon') + self.validator(month_style='Klingon') except TypeError as e: - self.assertTrue( - str(e) == "Bad month_style: 'klingon'") + self.assertEqual(str(e), "Bad month_style: 'klingon'") else: self.fail('month_style should be invalid') try: - dc = self.validator(month_style='ydm') + self.validator(month_style='ydm') except TypeError as e: - self.assertTrue( - str(e) == "Bad month_style: 'ydm'") + self.assertEqual( + str(e), "Bad month_style: 'ydm'") else: self.fail('month_style should be invalid') @@ -255,14 +248,14 @@ def test_us_style(self): try: self.assertEqual(dc.to_python('20/12/2007'), d) except Invalid as e: - self.assertTrue('Please enter a month from 1 to 12' in str(e)) + self.assertIn('Please enter a month from 1 to 12', str(e)) else: self.fail('Date should be invalid') try: self.assertEqual(dc.to_python('12/Dec/2007'), d) except Invalid as e: - self.assertTrue( - 'Please enter the date in the form MM/DD/YYYY' in str(e)) + self.assertIn( + 'Please enter the date in the form MM/DD/YYYY', str(e)) else: self.fail('Date should be invalid') @@ -276,14 +269,14 @@ def test_euro_style(self): try: self.assertEqual(dc.to_python('12/20/2007'), d) except Invalid as e: - self.assertTrue('Please enter a month from 1 to 12' in str(e)) + self.assertIn('Please enter a month from 1 to 12', str(e)) else: self.fail('Date should be invalid') try: self.assertEqual(dc.to_python('Dec/12/2007'), d) except Invalid as e: - self.assertTrue( - 'Please enter the date in the form DD/MM/YYYY' in str(e)) + self.assertIn( + 'Please enter the date in the form DD/MM/YYYY', str(e)) else: self.fail('Date should be invalid') @@ -297,14 +290,14 @@ def test_iso_style(self): try: self.assertEqual(dc.to_python('2013/30/06'), d) except Invalid as e: - self.assertTrue('Please enter a month from 1 to 12' in str(e)) + self.assertIn('Please enter a month from 1 to 12', str(e)) else: self.fail('Date should be invalid') try: self.assertEqual(dc.to_python('2013/06/Jun'), d) except Invalid as e: - self.assertTrue( - 'Please enter the date in the form YYYY/MM/DD' in str(e)) + self.assertIn( + 'Please enter the date in the form YYYY/MM/DD', str(e)) else: self.fail('Date should be invalid') @@ -318,21 +311,21 @@ def test_no_day(self): try: self.assertEqual(dc.to_python('20/2007'), d) except Invalid as e: - self.assertTrue('Please enter a month from 1 to 12' in str(e)) + self.assertIn('Please enter a month from 1 to 12', str(e)) else: self.fail('Date should be invalid') try: self.assertEqual(dc.to_python('12/20/2007'), d) except Invalid as e: - self.assertTrue( - 'Please enter the date in the form MM/YYYY' in str(e)) + self.assertIn( + 'Please enter the date in the form MM/YYYY', str(e)) else: self.fail('Date should be invalid') try: self.assertEqual(dc.to_python('2007/Dec'), d) except Invalid as e: - self.assertTrue( - 'Please enter the date in the form MM/YYYY' in str(e)) + self.assertIn( + 'Please enter the date in the form MM/YYYY', str(e)) else: self.fail('Date should be invalid') @@ -382,30 +375,30 @@ def test_time(self): try: tc.to_python('25:30:15') except Invalid as e: - self.assertTrue( - 'You must enter an hour in the range 0-23' in str(e)) + self.assertIn( + 'You must enter an hour in the range 0-23', str(e)) else: self.fail('Time should be invalid') try: tc.to_python('20:75:15') except Invalid as e: - self.assertTrue( - 'You must enter a minute in the range 0-59' in str(e)) + self.assertIn( + 'You must enter a minute in the range 0-59', str(e)) else: self.fail('Time should be invalid') try: tc.to_python('20:30:75') except Invalid as e: - self.assertTrue( - 'You must enter a second in the range 0-59' in str(e)) + self.assertIn( + 'You must enter a second in the range 0-59', str(e)) else: self.fail('Time should be invalid') try: tc.to_python('20:30:zx') except Invalid as e: - self.assertTrue( - 'The second value you gave is not a number' in str(e)) - self.assertTrue('zx' in str(e)) + self.assertIn( + 'The second value you gave is not a number', str(e)) + self.assertIn('zx', str(e)) else: self.fail('Time should be invalid') @@ -437,7 +430,7 @@ class State: values = validator.to_python(start_values, state) except Invalid as e: self.assertEqual(e.unpack_errors(), - {'people': u'Please add a person'}) + {'people': 'Please add a person'}) else: raise Exception("Shouldn't be valid data", values, start_values) @@ -534,8 +527,8 @@ def test_numerals_at_beggining(self): def test_non_english_inames(self): """i-names with non-English characters are valid""" - self.validator.to_python(u'=Gustavo.Narea.García') - self.validator.to_python(u'@名前.例') + self.validator.to_python('=Gustavo.Narea.García') + self.validator.to_python('@名前.例') def test_inames_plus_paths(self): """i-names with paths are valid but not supported""" @@ -544,9 +537,9 @@ def test_inames_plus_paths(self): def test_communities(self): """i-names may have so-called 'communities'""" - self.validator.to_python(u'=María*Yolanda*Liliana*Gustavo') - self.validator.to_python(u'=Gustavo*Andreina') - self.validator.to_python(u'@IBM*Lenovo') + self.validator.to_python('=María*Yolanda*Liliana*Gustavo') + self.validator.to_python('=Gustavo*Andreina') + self.validator.to_python('@IBM*Lenovo') class TestINumberValidator(unittest.TestCase): @@ -615,12 +608,12 @@ class TestOneOfValidator(unittest.TestCase): def test_docstring(self): doc = validators.OneOf.__doc__ - self.assertTrue( - 'Value must be one of: ``%(items)s`` (not ``%(value)r``)' in doc) + self.assertIn( + 'Value must be one of: ``%(items)s`` (not ``%(value)r``)', doc) def test_unicode_list(self): - o = validators.OneOf([u'ö', u'a']) - self.assertRaises(Invalid, o.to_python, u"ä") + o = validators.OneOf(['ö', 'a']) + self.assertRaises(Invalid, o.to_python, "ä") def test_ascii_list(self): o = validators.OneOf(['a', 'b']) @@ -636,6 +629,29 @@ class foo(Schema): self.assertEqual(expected, value) + def test_mixed_list(self): + o = validators.OneOf(['a', 'b', 'ö', '™', 1, 2, 3]) + try: + o.to_python('c') + raise ValueError("exception expected") + except Invalid as e: + self.assertIn("Value must be one of: a; b; ö; ™; 1; 2; 3 (not 'c')", str(e)) + try: + o.to_python(9) + raise ValueError("exception expected") + except Invalid as e: + self.assertIn("Value must be one of: a; b; ö; ™; 1; 2; 3 (not 9)", str(e)) + try: + o.to_python('d') + raise ValueError("exception expected") + except Invalid as e: + self.assertIn("Value must be one of: a; b; ö; ™; 1; 2; 3 (not 'd')", str(e)) + try: + o.to_python('§') + raise ValueError("exception expected") + except Invalid as e: + self.assertIn("Value must be one of: a; b; ö; ™; 1; 2; 3 (not '§')", str(e)) + class TestIPAddressValidator(unittest.TestCase): @@ -659,13 +675,13 @@ def test_leading_zeros(self): try: validate('1.2.3.037') except Invalid as e: - self.assertTrue('The octets must not have leading zeros' in str(e)) + self.assertIn('The octets must not have leading zeros', str(e)) else: self.fail('IP address octets with leading zeros should be invalid') try: validate('1.2.3.0377') except Invalid as e: - self.assertTrue('The octets must not have leading zeros' in str(e)) + self.assertIn('The octets must not have leading zeros', str(e)) else: self.fail('IP octets with leading zeros should be invalid') @@ -673,13 +689,14 @@ def test_leading_zeros_allowed(self): validate = self.validator(leading_zeros=True).to_python try: validate('1.2.3.037') - except Invalid as e: + except Invalid: self.fail('IP address octets with leading zeros should be valid') try: validate('1.2.3.0377') except Invalid as e: - self.assertTrue("The octets must be within the range of 0-255" - " (not '377')" in str(e)) + self.assertIn( + "The octets must be within the range of 0-255 (not '377')", + str(e)) else: self.fail( 'IP address octets should not be interpreted as octal numbers') @@ -691,8 +708,9 @@ def setUp(self): self.validator = validators.URL() def test_cojp(self): - self.assertEqual(self.validator.to_python('http://domain.co.jp'), - 'http://domain.co.jp') + self.assertEqual( + self.validator.to_python('http://domain.co.jp'), + 'http://domain.co.jp') def test_1char_thirdlevel(self): self.assertEqual(self.validator.to_python( @@ -704,6 +722,10 @@ def test_ip_validator(self): "http://65.18.195.155/cgi-ordoro/bo/start.cgi"), "http://65.18.195.155/cgi-ordoro/bo/start.cgi") + def test_invalid_url(self): + self.assertRaises(validators.Invalid, self.validator.to_python, + '[http://domain.co.jp') + class TestRequireIfMissingValidator(unittest.TestCase): @@ -714,7 +736,7 @@ def test_missing(self): v = self.validator('phone_type', missing='mail') self.assertEqual( validate(v, dict(phone_type='')), - dict(phone_type=u'Please enter a value')) + dict(phone_type='Please enter a value')) self.assertEqual( validate(v, dict(phone_type='', mail='foo@bar.org')), dict(phone_type='', mail='foo@bar.org')) @@ -723,7 +745,7 @@ def test_present(self): v = self.validator('phone_type', present='phone') self.assertEqual( validate(v, dict(phone_type='', phone='510 420 4577')), - dict(phone_type=u'Please enter a value')) + dict(phone_type='Please enter a value')) self.assertEqual( validate(v, dict(phone='')), dict(phone='')) @@ -731,7 +753,8 @@ def test_zero(self): v = self.validator('operator', present='operand') self.assertEqual( validate(v, dict(operator='', operand=0)), - dict(operator=u'Please enter a value')) + dict(operator='Please enter a value')) + class TestRequireIfMatchingValidator(unittest.TestCase): @@ -749,14 +772,14 @@ def test_matching(self): v = self.validator('phone_type', 'mobile', required_fields=['mobile']) self.assertEqual( validate(v, dict(phone_type='mobile')), - dict(mobile=u'Please enter a value') + dict(mobile='Please enter a value') ) def test_matching_multiple_required(self): v = self.validator('phone_type', 'mobile', required_fields=['mobile', 'code']) self.assertEqual( validate(v, dict(phone_type='mobile')), - dict(mobile=u'Please enter a value') + dict(mobile='Please enter a value') ) def test_not_matching(self): diff --git a/formencode/tests/test_variabledecode.py b/tests/test_variabledecode.py similarity index 90% rename from formencode/tests/test_variabledecode.py rename to tests/test_variabledecode.py index a281ddf9..6fe53f58 100644 --- a/formencode/tests/test_variabledecode.py +++ b/tests/test_variabledecode.py @@ -47,6 +47,12 @@ def test_dict_list_dict(self): self.assertEqual(expect, variable_decode(src)) + def test_list_dict_bad_key(self): + src = {'a-0.good': 'a', 'a.0.bad': 'b', 'a-1.good': 'c'} + expect = {'a': [{'good': 'a'}, {'good': 'c'}, {'bad': 'b'}]} + + self.assertEqual(expect, variable_decode(src)) + class TestVariableEncode(unittest.TestCase): diff --git a/tox.ini b/tox.ini index db74de4a..64b7f69b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,24 +1,26 @@ [tox] -envlist=py26,py27,pypy,py33,py34 +envlist=py3{7,8,9,10,11,12},pypy3,flake8,docs -[testenv] -deps=nose - dnspython - pycountry -commands=nosetests +[testenv:docs] +basepython = python3.11 +deps = + pycountry + sphinx +commands = + sphinx-build -b html -nEW docs docs/_build/html -[testenv:py33] -deps=nose - dnspython3 - pycountry -# Need to run nosetests in a different directory so it picks up 2to3-translated -# files and not the untranslated files in the source directory -commands=nosetests -w /tmp formencode +[testenv:flake8] +basepython = python3.11 +deps = + flake8 +commands = + flake8 src tests -[testenv:py34] -deps=nose - dnspython3 - pycountry -# Need to run nosetests in a different directory so it picks up 2to3-translated -# files and not the untranslated files in the source directory -commands=nosetests -w /tmp formencode +[testenv] +deps= + pytest + wheel + pycountry + dnspython >= 2 +commands= + pytest tests {posargs}