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 0e6e090a..619c6219 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,4 @@ docs/_build
.project
.pydevproject
.settings
+.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 637e3bea..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-language: python
-
-python:
- - 2.7
- - 3.6
- - 3.7
- - 3.8
- - pypy
-
-install: pip install .
-
-script: python setup.py nosetests
-
diff --git a/LICENSE.txt b/LICENSE.txt
index a611276f..20bd89b8 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,7 +1,5 @@
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
@@ -9,13 +7,9 @@ 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
diff --git a/MANIFEST.in b/MANIFEST.in
index 44df65a8..549bdeb8 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,26 @@
exclude .gitignore
exclude .gitattributes
-exclude .travis.yml
+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
+
+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 7c7a764c..76d97684 100644
--- a/README.rst
+++ b/README.rst
@@ -1,38 +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 `_.
Testing
-------
-Use `python setup.py nosetests` to run the test suite.
-Use `tox` to run the test suite for all supported Python versions.
+Use ``pytest formencode`` to run the test suite.
+Use ``tox`` to run the test suite for all supported Python versions.
Changes
-------
-Added a validator that can require one or more fields based on the value of another field.
-
-A German howto can be found here: http://techblog.auf-nach-mallorca.info/2014/08/19/dynamische_formulare_validieren_mit_formencode/
-
-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 044b9a89..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,29 +52,26 @@ 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)
+ >>> 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'])
- >>> import six
- >>> if not six.PY2: # 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 as e:
... print (e)
@@ -90,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 as e:
- ... print (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__.
@@ -151,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'
@@ -195,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
@@ -204,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 = [
@@ -258,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
@@ -286,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`_
@@ -299,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):
@@ -325,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
@@ -371,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.
@@ -382,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
-----
@@ -434,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:
@@ -495,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)
-------------------------------------
@@ -518,13 +517,13 @@ 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
@@ -537,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
@@ -573,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 9fde57c5..ac4a06dd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,6 +1,3 @@
-from __future__ import absolute_import
-# -*- coding: utf-8 -*-
-#
# FormEncode documentation build configuration file, created by
# sphinx-quickstart on Tue Aug 30 2011.
#
@@ -12,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 -----------------------------------------------------
@@ -34,30 +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 = '2.0.0'
from pkg_resources import get_distribution
release = get_distribution('formencode').version
-# for example take major/minor
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:
@@ -182,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
@@ -215,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/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-2.0.txt b/docs/whatsnew-2.0.txt
index 55b6233b..9a54e515 100644
--- a/docs/whatsnew-2.0.txt
+++ b/docs/whatsnew-2.0.txt
@@ -1,20 +1,50 @@
-What's New In FormEncode 2.0
-==============================
+What's New In FormEncode 2.x
+============================
-This article explains the latest changes in `FormEncode` version 2.0.0
-compared to its predecessor, `FormEncode` 1.3.0
+This article explains the latest changes in `FormEncode` version 2.x
+compared to its predecessor, `FormEncode` version 1.3.
+2.1.1
+-----
-Changelog
----------
+ - 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. If
- you need Python 2.6 or 3.2 to 3.5 compatibility please use FormEncode 1.3.
-You might also try FormEncode 2.0.0a1 which supports Python 2.6 and Python
-3.3-3.5
- - This will be the last major version to support Python 2.7
- - Add strict flag to USPostalCode to raise error on postal codes that has too
+ - `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
@@ -22,6 +52,6 @@ You might also try FormEncode 2.0.0a1 which supports Python 2.6 and Python
- 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
+ - 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__
+ - The version of the library can be checked using ``formencode.__version__``
diff --git a/examples/WebwareExamples/index.py b/examples/WebwareExamples/index.py
index c8b1a1ea..4a3fdd7b 100644
--- a/examples/WebwareExamples/index.py
+++ b/examples/WebwareExamples/index.py
@@ -1,9 +1,6 @@
-from __future__ import absolute_import
-from __future__ import print_function
from formencode import Invalid, htmlfill, Schema, validators
from WebKit.Page import Page
-import six
page_style = '''
@@ -76,8 +73,8 @@ def save(self):
try:
fields = FormSchema.to_python(fields, self)
except Invalid as e:
- errors = dict((k, v.encode('utf-8'))
- for k, v in six.iteritems(e.unpack_errors()))
+ 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)
diff --git a/formencode/__init__.py b/formencode/__init__.py
deleted file mode 100644
index b4d46e7d..00000000
--- a/formencode/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from __future__ import absolute_import
-# formencode package
-from pkg_resources import get_distribution, DistributionNotFound
-
-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
-
-try:
- __version__ = get_distribution(__name__).version
-except DistributionNotFound:
- # package is not installed
- __version__ = 'local-test'
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/tests/__init__.py b/formencode/tests/__init__.py
deleted file mode 100644
index 24f33978..00000000
--- a/formencode/tests/__init__.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from __future__ import absolute_import
-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 b2aba26e..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,14 +71,9 @@ input_file = formencode/i18n/FormEncode.pot
output_dir = formencode/i18n
previous = true
-[wheel]
-universal = 1
-
[pep8]
-max_line_length=100
+max_line_length=88
[flake8]
-max_line_length=100
-
-[metadata]
-license_files = LICENSE.txt
+max-line-length = 88
+extend-ignore = E128,E402,E501,E731
diff --git a/setup.py b/setup.py
index f0ec674d..0b62acc5 100755
--- a/setup.py
+++ b/setup.py
@@ -5,51 +5,6 @@
The official repo is at GitHub: https://github.com/formencode/formencode
"""
-from __future__ import absolute_import
+from setuptools import setup
-import sys
-from setuptools import setup, find_packages
-
-if not '2.7' <= sys.version < '3.0' and not '3.6' <= sys.version:
- raise ImportError('Python version not supported')
-
-tests_require = ['nose', 'dnspython==1.16.0' if sys.version < '3.0' else 'dnspython>=2.0.0',
- 'pycountry<19' if sys.version < '3.0' else 'pycountry']
-
-doctests = ['docs/htmlfill.txt', 'docs/Validator.txt',
- 'formencode/tests/non_empty.txt']
-
-setup(name='FormEncode',
- # requires_python='>=2.7,!=3.0,!=3.1,!=3.2,!=3.3,!=3.4,' # PEP345
- description="HTML form validation, generation, and conversion package",
- long_description=__doc__,
- classifiers=[
- "Development Status :: 4 - Beta",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: MIT License",
- "Programming Language :: Python",
- "Programming Language :: Python :: 2",
- "Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Topic :: Software Development :: Libraries :: Python Modules",
- ],
- author='Ian Bicking',
- author_email='ianb@colorstudy.com',
- url='http://formencode.org',
- license='MIT',
- data_files = [("", ["LICENSE.txt"])],
- zip_safe=False,
- packages=find_packages(),
- include_package_data=True,
- package_data={'formencode': ['../docs/*.txt']},
- test_suite='formencode.tests',
- install_requires=['six'],
- tests_require=tests_require,
- use_scm_version=True,
- setup_requires=['setuptools_scm', 'setuptools_scm_git_archive'],
-
- extras_require={'testing': tests_require},
- )
+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 80%
rename from formencode/api.py
rename to src/formencode/api.py
index da321fa2..88c62b39 100644
--- a/formencode/api.py
+++ b/src/formencode/api.py
@@ -1,55 +1,44 @@
"""
Core classes for validation.
"""
-from __future__ import absolute_import
-from . import declarative
import gettext
import os
import re
import textwrap
import warnings
-import six
-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,
@@ -59,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
@@ -92,7 +80,7 @@ def inner(*args, **kwargs):
return outer
-class NoDefault(object):
+class NoDefault:
"""A dummy value used for parameters with no default."""
@@ -149,18 +137,7 @@ def __init__(self, msg,
% (self, self.error_list, self.error_dict))
def __str__(self):
- val = self.msg
- return val
-
- if six.text_type is not str: # Python 2
-
- def __unicode__(self):
- if isinstance(self.msg, six.text_type):
- return self.msg
- elif isinstance(self.msg, str):
- return self.msg.decode('utf8')
- else:
- return six.text_type(self.msg)
+ return self.msg
def unpack_errors(self, encode_variables=False, dict_char='.',
list_char='-'):
@@ -179,9 +156,9 @@ def unpack_errors(self, encode_variables=False, dict_char='.',
for item in self.error_list]
if self.error_dict:
result = {}
- for name, item in six.iteritems(self.error_dict):
+ for name, item in self.error_dict.items():
result[name] = item if isinstance(
- item, six.string_types) else item.unpack_errors()
+ item, str) else item.unpack_errors()
if encode_variables:
from . import variabledecode
result = variabledecode.variable_encode(
@@ -196,9 +173,7 @@ def unpack_errors(self, encode_variables=False, dict_char='.',
return self.msg
-############################################################
-## Base Classes
-############################################################
+# Base Classes
class Validator(declarative.Declarative):
@@ -240,31 +215,6 @@ def to_python(self, value, state=None):
def from_python(self, value, state=None):
return value
- _message_vars_decode = None
- if six.text_type is not str:
- def _message_vars_decode(self, message_vars):
- """
- Under python2, a form value in web frameworks may be encoded as
- UTF8 unicode. The standard error templates use a ``%(value)r``
- string formatting, which will render the error as ``u"Foo"`` instead
- of just ``"Foo"``. This decoder, which can be overridden, will
- encode the value back to a python string and render ``"Foo"``. This
- decoder will only update the error dictionary if the unicode and
- string values are equal by comparison.
-
- v = message_vars.items()[0][1]
- """
- if six.text_type is not str:
- for (k, v) in message_vars.items():
- if isinstance(v, six.text_type):
- try:
- v2 = v.encode('utf-8')
- except Exception as e:
- v2 = v
- if v == v2:
- message_vars[k] = v2
- return message_vars
-
def message(self, msgName, state, **kw):
# determine translation function
try:
@@ -272,8 +222,8 @@ def message(self, msgName, state, **kw):
except AttributeError:
try:
if self.use_builtins_gettext:
- import six.moves.builtins
- trans = six.moves.builtins._
+ import builtins
+ trans = builtins._
else:
trans = _stdtrans
except AttributeError:
@@ -282,12 +232,7 @@ def message(self, msgName, state, **kw):
if not callable(trans):
trans = _stdtrans
- msg = self._messages[msgName]
- msg = trans(msg, **self.gettextargs)
-
- if self._message_vars_decode:
- # handle custom decoding for message vars
- kw = self._message_vars_decode(kw)
+ msg = trans(self._messages[msgName], **self.gettextargs)
try:
return msg % kw
@@ -340,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(six.iteritems(cls._messages))
- 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')
@@ -356,6 +300,7 @@ class _Identity(Validator):
def __repr__(self):
return 'validators.Identity'
+
Identity = _Identity()
@@ -482,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, six.string_types):
+ if self.strip and isinstance(value, str):
value = value.strip()
elif hasattr(value, 'mixed'):
# Support Paste's MultiDict
@@ -515,7 +459,7 @@ def to_python(self, value, state=None):
def from_python(self, value, state=None):
try:
- if self.strip and isinstance(value, six.string_types):
+ if self.strip and isinstance(value, str):
value = value.strip()
if not self.accept_python:
if self.is_empty(value):
@@ -551,7 +495,7 @@ def empty_value(self, value):
return None
def assert_string(self, value, state):
- if not isinstance(value, six.string_types):
+ 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 90%
rename from formencode/compound.py
rename to src/formencode/compound.py
index 6005f1a3..5caf950d 100644
--- a/formencode/compound.py
+++ b/src/formencode/compound.py
@@ -1,18 +1,13 @@
"""
Validators for applying validations in sequence.
"""
-from __future__ import absolute_import
-
-from .api import (FancyValidator, Identity, Invalid, NoDefault, Validator,
- is_validator)
-import six
+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)
@@ -40,14 +35,14 @@ class CompoundValidator(FancyValidator):
@staticmethod
def __classinit__(cls, new_attrs):
FancyValidator.__classinit__(cls, new_attrs)
- toAdd = []
- for name, value in six.iteritems(new_attrs):
+ 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)
@@ -135,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:
@@ -149,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::
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 128c9122..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,29 +41,27 @@ 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.
"""
-from __future__ import absolute_import
import threading
from itertools import count
-from six.moves import range
__all__ = ['Context', 'ContextRestoreError']
_restore_ids = count()
-class NoDefault(object):
+class NoDefault:
"""A dummy value used for parameters with no default."""
@@ -71,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()
@@ -95,7 +93,7 @@ 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 = next(_restore_ids)
@@ -153,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 95%
rename from formencode/declarative.py
rename to src/formencode/declarative.py
index 2375b3b9..550a2703 100644
--- a/formencode/declarative.py
+++ b/src/formencode/declarative.py
@@ -17,18 +17,14 @@
Also, you can define a __classinit__(cls, new_attrs) method, which
will be called when the class is created (including subclasses).
"""
-from __future__ import absolute_import
import copy
import types
from itertools import count
-import six
-from six.moves import map
-from six.moves import zip
-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
@@ -43,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
@@ -85,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
@@ -101,7 +97,7 @@ def __get__(self, obj, cls=None):
return types.MethodType(self.func, obj)
-class Declarative(six.with_metaclass(DeclarativeMeta, object)):
+class Declarative(metaclass=DeclarativeMeta):
__unpackargs__ = ()
@@ -145,7 +141,7 @@ 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 six.iteritems(kw):
+ for name, value in kw.items():
setattr(self, name, value)
if 'declarative_count' not in kw:
self.declarative_count = next(self.counter)
@@ -190,7 +186,7 @@ def __sourcerepr__(self, source, binding=None):
else:
break
args.extend('%s=%s' % (name, source.makeRepr(value))
- for (name, value) in six.iteritems(vals))
+ 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 90%
rename from formencode/doctest_xml_compare.py
rename to src/formencode/doctest_xml_compare.py
index ebac496d..395d697a 100644
--- a/formencode/doctest_xml_compare.py
+++ b/src/formencode/doctest_xml_compare.py
@@ -1,15 +1,7 @@
-from __future__ import absolute_import
-from __future__ import print_function
-
import doctest
import xml.etree.ElementTree as ET
-import six
-from six.moves import map
-from six.moves import zip
-try:
- XMLParseError = ET.ParseError
-except AttributeError: # Python < 2.7
- from xml.parsers.expat import ExpatError as XMLParseError
+
+XMLParseError = ET.ParseError
RealOutputChecker = doctest.OutputChecker
@@ -74,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 six.iteritems(x1.attrib):
+ for name, value in x1.attrib.items():
if x2.attrib.get(name) != value:
if reporter:
reporter('Attributes do not match: %s=%r, %s=%r'
@@ -125,7 +117,7 @@ def make_xml(s):
def make_string(xml):
- if isinstance(xml, six.string_types):
+ 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 50%
rename from formencode/fieldstorage.py
rename to src/formencode/fieldstorage.py
index 40a4d1b6..93b2ccd8 100644
--- a/formencode/fieldstorage.py
+++ b/src/formencode/fieldstorage.py
@@ -1,11 +1,6 @@
-## FormEncode, a Form processor
-## Copyright (C) 2003, Ian Bicking
"""
Wrapper class for use with cgi.FieldStorage types for file uploads
"""
-from __future__ import absolute_import
-
-import cgi
def convert_fieldstorage(fs):
diff --git a/formencode/foreach.py b/src/formencode/foreach.py
similarity index 91%
rename from formencode/foreach.py
rename to src/formencode/foreach.py
index 70abfc2b..96c064a1 100644
--- a/formencode/foreach.py
+++ b/src/formencode/foreach.py
@@ -1,11 +1,9 @@
"""
Validator for repeating items.
"""
-from __future__ import absolute_import
from .api import NoDefault, Invalid
from .compound import CompoundValidator, from_python
-import six
__all__ = ['ForEach']
@@ -85,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(six.text_type(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:
@@ -107,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
@@ -127,7 +124,7 @@ def __delete__(self, obj):
del _IfMissing
def _convert_to_list(self, value):
- if isinstance(value, six.string_types):
+ if isinstance(value, str):
return [value]
elif value is None:
return []
@@ -137,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 95%
rename from formencode/htmlfill.py
rename to src/formencode/htmlfill.py
index 90f4beda..a9fd12d9 100644
--- a/formencode/htmlfill.py
+++ b/src/formencode/htmlfill.py
@@ -1,12 +1,10 @@
"""
Parser for HTML forms, that fills in defaults and errors. See ``render``.
"""
-from __future__ import absolute_import
import re
from formencode.rewritingparser import RewritingParser, html_quote
-import six
__all__ = ['render', 'htmlliteral', 'default_formatter',
'none_formatter', 'escape_formatter',
@@ -70,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.
@@ -126,7 +124,7 @@ def render(form, defaults=None, errors=None, use_all_keys=False,
return p.text()
-class htmlliteral(object):
+class htmlliteral:
def __init__(self, html, text=None):
if text is None:
@@ -247,7 +245,7 @@ def __init__(self, defaults, errors=None, use_all_keys=False,
self.in_select = None
self.skip_next = False
self.errors = errors or {}
- if isinstance(self.errors, six.string_types):
+ if isinstance(self.errors, str):
self.errors = {None: self.errors}
self.in_error = None
self.skip_error = False
@@ -276,14 +274,11 @@ def str_compare(self, str1, str2):
Compare the two objects as strings (coercing to strings if necessary).
Also uses encoding to compare the strings.
"""
- if not isinstance(str1, six.string_types):
- if hasattr(str1, '__unicode__'):
- str1 = six.text_type(str1)
- else:
- str1 = str(str1)
- if type(str1) == type(str2):
+ if not isinstance(str1, str):
+ str1 = str(str1)
+ if type(str1) is type(str2):
return str1 == str2
- if isinstance(str1, six.text_type):
+ if isinstance(str1, str):
str1 = str1.encode(self.encoding or self.default_encoding)
else:
str2 = str2.encode(self.encoding or self.default_encoding)
@@ -297,7 +292,7 @@ def close(self):
if key in unused_errors:
del unused_errors[key]
if self.auto_error_formatter:
- for key, value in six.iteritems(unused_errors):
+ for key, value in unused_errors.items():
error_message = self.auto_error_formatter(value)
error_message = '\n%s' % (key, error_message)
self.insert_at_marker(
@@ -320,9 +315,6 @@ def close(self):
if self.encoding is not None:
new_content = []
for item in self._content:
- if (six.text_type is not str # Python 2
- and isinstance(item, str)):
- item = item.decode(self.encoding)
new_content.append(item)
self._content = new_content
self._text = self._get_text()
@@ -422,11 +414,8 @@ def handle_input(self, attrs, startend):
if self.prefix_error:
self.write_marker(name)
value = self.defaults.get(name)
- if (six.text_type is not str # Python 2
- and isinstance(name, six.text_type) and isinstance(value, str)):
- value = value.decode(self.encoding or self.default_encoding)
if name in self.add_attributes:
- for attr_name, attr_value in six.iteritems(self.add_attributes[name]):
+ for attr_name, attr_value in self.add_attributes[name].items():
if attr_name.startswith('+'):
attr_name = attr_name[1:]
self.set_attr(attrs, attr_name,
@@ -572,7 +561,7 @@ def selected_multiple(self, obj, value):
"""
if obj is None:
return False
- if isinstance(obj, six.string_types):
+ if isinstance(obj, str):
return obj == value
if hasattr(obj, '__contains__'):
if value in obj:
diff --git a/formencode/htmlfill_schemabuilder.py b/src/formencode/htmlfill_schemabuilder.py
similarity index 93%
rename from formencode/htmlfill_schemabuilder.py
rename to src/formencode/htmlfill_schemabuilder.py
index 2059ff79..5519a505 100644
--- a/formencode/htmlfill_schemabuilder.py
+++ b/src/formencode/htmlfill_schemabuilder.py
@@ -6,8 +6,6 @@
``listen`` argument), or call ``parse_schema`` to just parse out a
``Schema`` object.
"""
-from __future__ import absolute_import
-
from . import validators
from . import schema
from . import compound
@@ -29,9 +27,8 @@ def parse_schema(form):
return listener.schema()
-default_validators = dict(
- [(name.lower(), getattr(validators, name))
- for name in dir(validators)])
+default_validators = {
+ name.lower(): getattr(validators, name) for name in dir(validators)}
def get_messages(cls, message):
@@ -63,7 +60,7 @@ def force_list(v):
return [v]
-class SchemaBuilder(object):
+class SchemaBuilder:
def __init__(self, validators=default_validators):
self.validators = validators
@@ -97,7 +94,7 @@ def listen_input(self, parser, tag, attrs):
if required:
v.validators.append(
validators.NotEmpty(
- messages=get_messages(validators.NotEmpty, message)))
+ messages=get_messages(validators.NotEmpty, message)))
else:
v.validators[0].if_missing = False
if add_to_end:
diff --git a/formencode/htmlgen.py b/src/formencode/htmlgen.py
similarity index 78%
rename from formencode/htmlgen.py
rename to src/formencode/htmlgen.py
index 6ab61736..c41d526c 100644
--- a/formencode/htmlgen.py
+++ b/src/formencode/htmlgen.py
@@ -55,17 +55,10 @@
'return to top'
"""
-from __future__ import absolute_import
import xml.etree.ElementTree as ET
-import six
-from six.moves import map
-
-try:
- from html import escape
-except ImportError: # Python < 3.2
- from cgi import escape
+from html import escape
__all__ = ['html']
@@ -93,26 +86,19 @@ def __call__(self, *args):
def quote(self, arg):
if arg is None:
return ''
- if six.text_type is not str: # Python 2
- arg = six.text_type(arg).encode(default_encoding)
return escape(arg, True)
def str(self, arg, encoding=None):
- if isinstance(arg, six.string_types):
- if not isinstance(arg, str):
- arg = arg.encode(default_encoding)
+ if isinstance(arg, str):
return arg
- elif arg is None:
+ if isinstance(str, bytes):
+ return arg.encode(default_encoding)
+ if arg is None:
return ''
- elif isinstance(arg, (list, tuple)):
+ if isinstance(arg, (list, tuple)):
return ''.join(map(self.str, arg))
- elif isinstance(arg, Element):
- return str(arg)
- else:
- arg = six.text_type(arg)
- if not isinstance(arg, str): # Python 2
- arg = arg.encode(default_encoding)
- return arg
+ return str(arg)
+
html = _HTML()
@@ -134,7 +120,7 @@ def __call__(self, *args, **kw):
if value is None:
del kw[name]
continue
- kw[name] = six.text_type(value)
+ kw[name] = str(value)
if name.endswith('_'):
kw[name[:-1]] = value
del kw[name]
@@ -154,33 +140,22 @@ def __call__(self, *args, **kw):
if not ET.iselement(arg):
if last is None:
if el.text is None:
- el.text = six.text_type(arg)
+ el.text = str(arg)
else:
- el.text += six.text_type(arg)
+ el.text += str(arg)
else:
if last.tail is None:
- last.tail = six.text_type(arg)
+ last.tail = str(arg)
else:
- last.tail += six.text_type(arg)
+ last.tail += str(arg)
else:
last = arg
el.append(last)
return el
- if six.text_type is str: # Python 3
-
- def __str__(self):
- return ET.tostring(
- self, default_encoding).decode(default_encoding)
-
- else:
-
- def __str__(self):
- return ET.tostring(self, default_encoding)
-
- def __unicode__(self):
- # This is lame!
- return str(self).decode(default_encoding)
+ def __str__(self):
+ return ET.tostring(
+ self, default_encoding).decode(default_encoding)
def __repr__(self):
content = str(self)
diff --git a/formencode/htmlrename.py b/src/formencode/htmlrename.py
similarity index 98%
rename from formencode/htmlrename.py
rename to src/formencode/htmlrename.py
index e8a5c38f..d884848e 100644
--- a/formencode/htmlrename.py
+++ b/src/formencode/htmlrename.py
@@ -1,8 +1,6 @@
"""
Module to rename form fields
"""
-from __future__ import absolute_import
-
from formencode.rewritingparser import RewritingParser
__all__ = ['rename', 'add_prefix']
diff --git a/formencode/i18n/FormEncode.pot b/src/formencode/i18n/FormEncode.pot
similarity index 100%
rename from formencode/i18n/FormEncode.pot
rename to src/formencode/i18n/FormEncode.pot
diff --git a/formencode/i18n/cs/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/cs/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/cs/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/cs/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/cs/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/cs/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/cs/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/cs/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/da/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/da/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/da/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/da/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/da/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/da/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/da/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/da/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/de/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/de/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/de/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/de/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/de/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/de/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/de/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/de/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/el/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/el/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/el/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/el/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/el/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/el/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/el/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/el/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/es/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/es/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/es/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/es/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/es/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/es/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/es/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/es/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/et/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/et/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/et/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/et/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/et/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/et/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/et/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/et/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/fi/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/fi/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/fi/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/fi/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/fi/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/fi/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/fi/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/fi/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/fr/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/fr/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/fr/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/fr/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/fr/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/fr/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/fr/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/fr/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/it/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/it/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/it/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/it/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/it/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/it/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/it/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/it/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/ja/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/ja/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/ja/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/ja/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/ja/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/ja/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/ja/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/ja/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/ko/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/ko/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/ko/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/ko/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/ko/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/ko/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/ko/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/ko/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/lt/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/lt/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/lt/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/lt/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/lt/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/lt/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/lt/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/lt/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/mk/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/mk/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/mk/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/mk/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/mk/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/mk/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/mk/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/mk/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/nb_NO/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/nb_NO/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/nb_NO/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/nb_NO/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/nb_NO/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/nb_NO/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/nb_NO/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/nb_NO/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/nl/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/nl/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/nl/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/nl/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/nl/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/nl/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/nl/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/nl/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/pl/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/pl/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/pl/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/pl/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/pl/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/pl/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/pl/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/pl/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/pt_BR/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/pt_BR/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/pt_BR/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/pt_BR/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/pt_BR/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/pt_BR/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/pt_BR/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/pt_BR/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/pt_PT/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/pt_PT/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/pt_PT/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/pt_PT/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/pt_PT/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/pt_PT/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/pt_PT/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/pt_PT/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/ru/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/ru/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/ru/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/ru/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/ru/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/ru/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/ru/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/ru/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/sk/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/sk/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/sk/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/sk/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/sk/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/sk/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/sk/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/sk/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/sl/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/sl/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/sl/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/sl/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/sl/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/sl/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/sl/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/sl/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/sr/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/sr/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/sr/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/sr/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/sr/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/sr/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/sr/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/sr/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/sr@latin/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/sr@latin/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/sr@latin/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/sr@latin/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/sr@latin/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/sr@latin/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/sr@latin/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/sr@latin/LC_MESSAGES/FormEncode.po
diff --git a/src/formencode/i18n/sv/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/sv/LC_MESSAGES/FormEncode.mo
new file mode 100644
index 00000000..5f3abb77
Binary files /dev/null and b/src/formencode/i18n/sv/LC_MESSAGES/FormEncode.mo differ
diff --git a/formencode/i18n/sv/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/sv/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/sv/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/sv/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/tr/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/tr/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/tr/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/tr/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/tr/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/tr/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/tr/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/tr/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/zh_CN/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/zh_CN/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/zh_CN/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/zh_CN/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/zh_CN/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/zh_CN/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/zh_CN/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/zh_CN/LC_MESSAGES/FormEncode.po
diff --git a/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.mo b/src/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.mo
similarity index 100%
rename from formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.mo
rename to src/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.mo
diff --git a/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.po b/src/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.po
similarity index 100%
rename from formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.po
rename to src/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.po
diff --git a/formencode/interfaces.py b/src/formencode/interfaces.py
similarity index 97%
rename from formencode/interfaces.py
rename to src/formencode/interfaces.py
index 2d8cfae4..d110c974 100644
--- a/formencode/interfaces.py
+++ b/src/formencode/interfaces.py
@@ -3,14 +3,14 @@
"""
-class Attribute(object):
+class Attribute:
def __init__(self, description, name=None):
self.description = description
self.name = name
-class Interface(object):
+class Interface:
pass
diff --git a/formencode/javascript/ordering.js b/src/formencode/javascript/ordering.js
similarity index 100%
rename from formencode/javascript/ordering.js
rename to src/formencode/javascript/ordering.js
diff --git a/formencode/national.py b/src/formencode/national.py
similarity index 96%
rename from formencode/national.py
rename to src/formencode/national.py
index a7a76ed9..b78256f0 100644
--- a/formencode/national.py
+++ b/src/formencode/national.py
@@ -1,13 +1,11 @@
"""
Country specific validators for use with FormEncode.
"""
-from __future__ import absolute_import
import re
from .api import FancyValidator
from .compound import Any
from .validators import Regex, Invalid, _
-import six
try:
import pycountry
@@ -25,9 +23,7 @@
no_country = ('Please easy_install pycountry or validators handling'
' country names and/or languages will not work.')
-############################################################
-## country lists and functions
-############################################################
+# country lists and functions
country_additions = [
('BY', _('Belarus')),
@@ -142,9 +138,7 @@ def get_language(code):
return _l(pycountry.languages.get(alpha_2=code).name)
-############################################################
-## country, state and postal code validators
-############################################################
+# country, state and postal code validators
class DelimitedDigitsPostalCode(Regex):
"""
@@ -194,8 +188,7 @@ class DelimitedDigitsPostalCode(Regex):
def assembly_formatstring(self, partition_lengths, delimiter):
if len(partition_lengths) == 1:
return _('%d digits') % partition_lengths[0]
- else:
- return delimiter.join('n' * l for l in partition_lengths)
+ return delimiter.join('n' * n for n in partition_lengths)
def assembly_grouping(self, partition_lengths, delimiter):
digit_groups = ['%s' * length for length in partition_lengths]
@@ -213,7 +206,7 @@ def assembly_regex(self, partition_lengths, delimiter, strict):
def __init__(self, partition_lengths, delimiter=None, strict=False,
*args, **kw):
- if isinstance(partition_lengths, six.integer_types):
+ if isinstance(partition_lengths, int):
partition_lengths = [partition_lengths]
if not delimiter:
delimiter = ''
@@ -405,8 +398,8 @@ def _convert_to_python(self, value, state):
value, state)
value = match.group(1).upper()
if not value.startswith('BFPO'):
- value = value.replace(' ', '')
- value = '%s %s' % (value[:-3], value[-3:])
+ value = ''.join(value.split())
+ value = '%s %s' % (value[:-3], value[-3:])
return value
@@ -421,19 +414,19 @@ class CountryValidator(FancyValidator):
::
>>> CountryValidator.to_python('Germany')
- u'DE'
+ 'DE'
>>> CountryValidator.to_python('Finland')
- u'FI'
+ 'FI'
>>> CountryValidator.to_python('UNITED STATES')
- u'US'
+ 'US'
>>> CountryValidator.to_python('Krakovia')
Traceback (most recent call last):
...
Invalid: That country is not listed in ISO 3166
>>> CountryValidator.from_python('DE')
- u'Germany'
+ 'Germany'
>>> CountryValidator.from_python('FI')
- u'Finland'
+ 'Finland'
"""
key_ok = True
@@ -617,9 +610,7 @@ def _convert_to_python(self, value, state):
return str(value).strip().upper()
-############################################################
-## phone number validators
-############################################################
+# phone number validators
class USPhoneNumber(FancyValidator):
"""
@@ -779,8 +770,7 @@ def _convert_to_python(self, value, state):
value = value.encode('ascii', 'strict')
except UnicodeEncodeError:
raise Invalid(self.message('phoneFormat', state), value, state)
- if six.text_type is str: # Python 3
- value = value.decode('ascii')
+ value = value.decode('ascii')
value = self._mark_chars_re.sub('-', value)
for f, t in [(' ', ' '),
('--', '-'), (' - ', '-'), ('- ', '-'), (' -', '-')]:
@@ -802,9 +792,7 @@ def _convert_to_python(self, value, state):
return value
-############################################################
-## language validators
-############################################################
+# language validators
class LanguageValidator(FancyValidator):
"""
@@ -823,17 +811,17 @@ class LanguageValidator(FancyValidator):
>>> l = LanguageValidator()
>>> l.to_python('German')
- u'de'
+ 'de'
>>> l.to_python('Chinese')
- u'zh'
+ 'zh'
>>> l.to_python('Klingonian')
Traceback (most recent call last):
...
Invalid: That language is not listed in ISO 639
>>> l.from_python('de')
- u'German'
+ 'German'
>>> l.from_python('zh')
- u'Chinese'
+ 'Chinese'
"""
key_ok = True
@@ -872,4 +860,5 @@ def validators():
return [name for name, value in list(globals().items())
if isinstance(value, type) and issubclass(value, FancyValidator)]
+
__all__ = ['Invalid'] + validators()
diff --git a/formencode/rewritingparser.py b/src/formencode/rewritingparser.py
similarity index 76%
rename from formencode/rewritingparser.py
rename to src/formencode/rewritingparser.py
index da852006..9d29b797 100644
--- a/formencode/rewritingparser.py
+++ b/src/formencode/rewritingparser.py
@@ -1,16 +1,8 @@
-from __future__ import absolute_import
-
-from six.moves import html_parser
import re
-import six
-from six.moves import range
-
-try:
- from html import escape
-except ImportError: # Python < 3.2
- from cgi import escape
-from six.moves.html_entities import name2codepoint
+from html import escape
+from html.entities import name2codepoint
+from html.parser import HTMLParser
def html_quote(v):
@@ -18,26 +10,19 @@ def html_quote(v):
return ''
if hasattr(v, '__html__'):
return v.__html__()
- if isinstance(v, six.string_types):
+ if isinstance(v, str):
return escape(v, True)
- if hasattr(v, '__unicode__'):
- v = six.text_type(v)
- else:
- v = str(v)
- return escape(v, True)
+ return escape(str(v), True)
-class RewritingParser(html_parser.HTMLParser):
+class RewritingParser(HTMLParser):
listener = None
skip_next = False
def __init__(self):
self._content = []
- try:
- html_parser.HTMLParser.__init__(self, convert_charrefs=False)
- except TypeError: # Python < 3.4
- html_parser.HTMLParser.__init__(self)
+ HTMLParser.__init__(self, convert_charrefs=False)
def feed(self, data):
self.data_is_str = isinstance(data, str)
@@ -46,7 +31,7 @@ def feed(self, data):
self.source_pos = 1, 0
if self.listener:
self.listener.reset()
- html_parser.HTMLParser.feed(self, data)
+ HTMLParser.feed(self, data)
_entityref_re = re.compile(r'&([a-zA-Z][-.a-zA-Z\d]*);')
_charref_re = re.compile(r'(\d+|[xX][a-fA-F\d]+);')
@@ -62,7 +47,7 @@ def _sub_entityref(self, match):
# If we don't recognize it, pass it through as though it
# wasn't an entity ref at all
return match.group(0)
- return six.unichr(name2codepoint[name])
+ return chr(name2codepoint[name])
def _sub_charref(self, match):
num = match.group(1)
@@ -70,7 +55,7 @@ def _sub_charref(self, match):
num = int(num[1:], 16)
else:
num = int(num)
- return six.unichr(num)
+ return chr(num)
def handle_misc(self, whatever):
self.write_pos()
@@ -163,13 +148,12 @@ def _get_text(self):
except UnicodeDecodeError as e:
if self.data_is_str:
e.reason += (
- " the form was passed in as an encoded string, but"
- " some data or error messages were unicode strings;"
- " the form should be passed in as a unicode string")
+ " the form was passed in as a byte string,"
+ " but some data or error messages were normal strings;"
+ " the form should be passed in as a string")
else:
e.reason += (
- " the form was passed in as an unicode string, but"
- " some data or error message was an encoded string;"
- " the data and error messages should be passed in as"
- " unicode strings")
+ " the form was passed in as a string, but some data"
+ " or error message was encoded; the data and error"
+ " messages should be passed in as strings")
raise
diff --git a/formencode/schema.py b/src/formencode/schema.py
similarity index 89%
rename from formencode/schema.py
rename to src/formencode/schema.py
index f7584971..7a14494d 100644
--- a/formencode/schema.py
+++ b/src/formencode/schema.py
@@ -1,14 +1,10 @@
-from __future__ import absolute_import
import warnings
from .api import _, is_validator, FancyValidator, Invalid, NoDefault
from . import declarative
from .exc import FERuntimeWarning
-import six
-from six.moves import map
-from six.moves import zip
-__all__ = ['Schema']
+__all__ = ['Schema', 'SimpleFormValidator']
class Schema(FancyValidator):
@@ -85,7 +81,7 @@ def __classinit__(cls, new_attrs):
# Scan through the class variables we've defined *just*
# for this subclass, looking for validators (both classes
# and instances):
- for key, value in six.iteritems(new_attrs):
+ for key, value in new_attrs.items():
if key in ('pre_validators', 'chained_validators'):
if is_validator(value):
msg = "Any validator with the name %s will be ignored." % \
@@ -100,12 +96,12 @@ def __classinit__(cls, new_attrs):
elif key in cls.fields:
del cls.fields[key]
- for name, value in six.iteritems(cls.fields):
+ for name, value in cls.fields.items():
cls.add_field(name, value)
def __initargs__(self, new_attrs):
self.fields = self.fields.copy()
- for key, value in six.iteritems(new_attrs):
+ for key, value in new_attrs.items():
if key in ('pre_validators', 'chained_validators'):
if is_validator(value):
msg = "Any validator with the name %s will be ignored." % \
@@ -249,9 +245,7 @@ def _convert_from_python(self, value_dict, state):
previous_full_dict = getattr(state, 'full_dict', None)
state.full_dict = value_dict
try:
- __traceback_info__ = None
- for name, value in six.iteritems(value_dict):
- __traceback_info__ = 'for_python in %s' % name
+ for name, value in value_dict.items():
try:
unused.remove(name)
except ValueError:
@@ -268,8 +262,6 @@ def _convert_from_python(self, value_dict, state):
except Invalid as e:
errors[name] = e
- del __traceback_info__
-
for name in unused:
validator = self.fields[name]
if state is not None:
@@ -290,7 +282,6 @@ def _convert_from_python(self, value_dict, state):
state.key = previous_key
for validator in pre:
- __traceback_info__ = 'for_python pre_validator %s' % validator
new = validator.from_python(new, state)
return new
@@ -331,53 +322,44 @@ def subvalidators(self):
result = []
result.extend(self.pre_validators)
result.extend(self.chained_validators)
- result.extend(six.itervalues(self.fields))
+ result.extend(self.fields.values())
return result
def is_empty(self, value):
- ## Generally nothing is empty for us
+ # generally nothing is empty for us
return False
def empty_value(self, value):
return {}
def _value_is_iterator(self, value):
- if isinstance(value, six.string_types + (six.binary_type, )):
+ if isinstance(value, (bytes, str)):
return False
- elif isinstance(value, (list, tuple)):
+ if isinstance(value, (list, tuple)):
return True
try:
for _v in value:
break
return True
- ## @@: Should this catch any other errors?:
- except TypeError:
+ except TypeError: # @@: should this catch any other errors?
return False
def format_compound_error(v, indent=0):
if isinstance(v, Exception):
- try:
- return str(v)
- except (UnicodeDecodeError, UnicodeEncodeError):
- # There doesn't seem to be a better way to get a str()
- # version if possible, and unicode() if necessary, because
- # testing for the presence of a __unicode__ method isn't
- # enough
- return six.text_type(v)
- elif isinstance(v, dict):
+ return str(v)
+ if isinstance(v, dict):
return ('%s\n' % (' ' * indent)).join(
'%s: %s' % (k, format_compound_error(value, indent=len(k) + 2))
- for k, value in sorted(six.iteritems(v)) if value is not None)
- elif isinstance(v, list):
+ for k, value in sorted(v.items()) if value is not None)
+ if isinstance(v, list):
return ('%s\n' % (' ' * indent)).join(
'%s' % (format_compound_error(value, indent=indent))
for value in v if value is not None)
- elif isinstance(v, six.string_types):
+ if isinstance(v, str):
return v
- else:
- assert False, "I didn't expect something like %s" % repr(v)
+ raise TypeError("I didn't expect something like %r" % v)
def merge_dicts(d1, d2):
@@ -387,15 +369,14 @@ def merge_dicts(d1, d2):
def merge_values(v1, v2):
- if isinstance(v1, six.string_types) and isinstance(v2, six.string_types):
+ if isinstance(v1, str) and isinstance(v2, str):
return v1 + '\n' + v2
- elif isinstance(v1, (list, tuple)) and isinstance(v2, (list, tuple)):
+ if isinstance(v1, (list, tuple)) and isinstance(v2, (list, tuple)):
return merge_lists(v1, v2)
- elif isinstance(v1, dict) and isinstance(v2, dict):
+ if isinstance(v1, dict) and isinstance(v2, dict):
return merge_dicts(v1, v2)
- else:
- # @@: Should we just ignore errors? Seems we do...
- return v1
+ # @@: Should we just ignore errors? Seems we do...
+ return v1
def merge_lists(l1, l2):
@@ -478,18 +459,15 @@ def to_python(self, value_dict, state):
errors = self.func(value_dict, state, self)
if not errors:
return value_dict
- if isinstance(errors, six.string_types):
+ if isinstance(errors, str):
raise Invalid(errors, value_dict, state)
- elif isinstance(errors, dict):
+ if isinstance(errors, dict):
raise Invalid(
format_compound_error(errors),
value_dict, state, error_dict=errors)
- elif isinstance(errors, Invalid):
+ if isinstance(errors, Invalid):
raise errors
- else:
- raise TypeError(
- "Invalid error value: %r" % errors)
- return value_dict
+ raise TypeError("Invalid error value: %r" % errors)
validate_partial = to_python
diff --git a/formencode/validators.py b/src/formencode/validators.py
similarity index 93%
rename from formencode/validators.py
rename to src/formencode/validators.py
index ff58642c..e17777f1 100644
--- a/formencode/validators.py
+++ b/src/formencode/validators.py
@@ -1,19 +1,17 @@
-## FormEncode, a Form processor
-## Copyright (C) 2003, Ian Bicking
+# FormEncode, a Form processor
+# Copyright (C) 2003, Ian Bicking
"""
Validator/Converters for use with FormEncode.
"""
-from __future__ import absolute_import
-import cgi
-import locale
+try:
+ import cgi
+except ImportError: # Python >= 3.13
+ cgi = None
import re
import warnings
from encodings import idna
-import six
-from six.moves import map
-from six.moves import range
try: # import dnspython
import dns.resolver
@@ -37,18 +35,16 @@
assert Identity and Invalid and NoDefault # silence unused import warnings
# Dummy i18n translation function, nothing is translated here.
-# Instead this is actually done in api.message.
+# Instead, this is actually done in api.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.
_ = lambda s: s
-############################################################
-## Utility methods
-############################################################
-
+# Utility methods
# These all deal with accepting both datetime and mxDateTime modules and types
+
datetime_module = None
mxDateTime_module = None
@@ -99,9 +95,7 @@ def datetime_isotime(module):
return module.ISO.Time
-############################################################
-## Wrapper Validators
-############################################################
+# Wrapper Validators
class ConfirmType(FancyValidator):
"""
@@ -314,9 +308,7 @@ def _convert_to_python(self, value, state):
_convert_from_python = _convert_to_python
-############################################################
-## Normal validators
-############################################################
+# Normal validators
class MaxLength(FancyValidator):
"""
@@ -517,13 +509,13 @@ class Regex(FancyValidator):
def __init__(self, *args, **kw):
FancyValidator.__init__(self, *args, **kw)
- if isinstance(self.regex, six.string_types):
+ if isinstance(self.regex, str):
ops = 0
- assert not isinstance(self.regexOps, six.string_types), (
+ assert not isinstance(self.regexOps, str), (
"regexOps should be a list of options from the re module "
"(names, or actual values)")
for op in self.regexOps:
- if isinstance(op, six.string_types):
+ if isinstance(op, str):
ops |= getattr(re, op)
else:
ops |= op
@@ -531,13 +523,13 @@ def __init__(self, *args, **kw):
def _validate_python(self, value, state):
self.assert_string(value, state)
- if self.strip and isinstance(value, six.string_types):
+ if self.strip and isinstance(value, str):
value = value.strip()
if not self.regex.search(value):
raise Invalid(self.message('invalid', state), value, state)
def _convert_to_python(self, value, state):
- if self.strip and isinstance(value, six.string_types):
+ if self.strip and isinstance(value, str):
return value.strip()
return value
@@ -619,7 +611,7 @@ def _validate_python(self, value, state):
try:
items = '; '.join(map(str, self.list))
except UnicodeError:
- items = '; '.join(map(six.text_type, self.list))
+ items = '; '.join(map(str, self.list))
raise Invalid(
self.message('notIn', state,
items=items, value=value), value, state)
@@ -694,16 +686,15 @@ def _convert_to_python(self, value, state):
state, items=items), value, state)
def _convert_from_python(self, value, state):
- for k, v in six.iteritems(self.dict):
+ for k, v in self.dict.items():
if value == v:
return k
if self.hideDict:
raise Invalid(self.message('valueNotFound', state), value, state)
- else:
- items = '; '.join(map(repr, six.itervalues(self.dict)))
- raise Invalid(
- self.message('chooseValue', state,
- value=repr(value), items=items), value, state)
+ items = '; '.join(map(repr, self.dict.values()))
+ raise Invalid(
+ self.message('chooseValue', state,
+ value=repr(value), items=items), value, state)
class IndexListConverter(FancyValidator):
@@ -816,13 +807,6 @@ class DateValidator(FancyValidator):
def _validate_python(self, value, state):
date_format = self.message('date_format', state)
- if (str is not six.text_type # Python 2
- and isinstance(date_format, six.text_type)):
- # strftime uses the locale encoding, not Unicode
- encoding = locale.getlocale(locale.LC_TIME)[1] or 'utf-8'
- date_format = date_format.encode(encoding)
- else:
- encoding = None
if self.earliest_date:
if callable(self.earliest_date):
earliest_date = self.earliest_date()
@@ -830,8 +814,6 @@ def _validate_python(self, value, state):
earliest_date = self.earliest_date
if value < earliest_date:
date_formatted = earliest_date.strftime(date_format)
- if encoding:
- date_formatted = date_formatted.decode(encoding)
raise Invalid(
self.message('after', state, date=date_formatted),
value, state)
@@ -842,8 +824,6 @@ def _validate_python(self, value, state):
latest_date = self.latest_date
if value > latest_date:
date_formatted = latest_date.strftime(date_format)
- if encoding:
- date_formatted = date_formatted.decode(encoding)
raise Invalid(
self.message('before', state, date=date_formatted),
value, state)
@@ -852,8 +832,6 @@ def _validate_python(self, value, state):
now = datetime_now(dt_mod)
if value < now:
date_formatted = now.strftime(date_format)
- if encoding:
- date_formatted = date_formatted.decode(encoding)
raise Invalid(
self.message('future', state, date=date_formatted),
value, state)
@@ -866,8 +844,6 @@ def _validate_python(self, value, state):
dt_mod, value.year, value.month, value.day)
if value_as_date < today:
date_formatted = now.strftime(date_format)
- if encoding:
- date_formatted = date_formatted.decode(encoding)
raise Invalid(
self.message('future', state, date=date_formatted),
value, state)
@@ -1012,8 +988,6 @@ def _convert_to_python(self, value, state):
class ByteString(FancyValidator):
"""Convert to byte string, treating empty things as the empty string.
- Under Python 2.x you can also use the alias `String` for this validator.
-
Also takes a `max` and `min` argument, and the string length must fall
in that range.
@@ -1065,27 +1039,27 @@ def __initargs__(self, new_attrs):
def _convert_to_python(self, value, state):
if value is None:
value = ''
- elif not isinstance(value, six.string_types):
+ elif not isinstance(value, str):
try:
value = bytes(value)
except UnicodeEncodeError:
- value = six.text_type(value)
- if self.encoding is not None and isinstance(value, six.text_type):
+ value = str(value)
+ if self.encoding is not None and isinstance(value, str):
value = value.encode(self.encoding)
return value
def _convert_from_python(self, value, state):
if value is None:
value = ''
- elif not isinstance(value, six.string_types):
+ elif not isinstance(value, str):
if isinstance(value, (list, tuple)):
value = self.list_joiner.join(
self._convert_from_python(v, state) for v in value)
try:
value = str(value)
except UnicodeEncodeError:
- value = six.text_type(value)
- if self.encoding is not None and isinstance(value, six.text_type):
+ value = str(value)
+ if self.encoding is not None and isinstance(value, str):
value = value.encode(self.encoding)
if self.strip:
value = value.strip()
@@ -1096,11 +1070,8 @@ def _validate_other(self, value, state):
return
if value is None:
value = ''
- elif not isinstance(value, six.string_types):
- try:
- value = str(value)
- except UnicodeEncodeError:
- value = six.text_type(value)
+ elif not isinstance(value, str):
+ value = str(value)
if self.max is not None and len(value) > self.max:
raise Invalid(
self.message('tooLong', state, max=self.max), value, state)
@@ -1117,10 +1088,10 @@ class UnicodeString(ByteString):
This is implemented as a specialization of the ByteString class.
- Under Python 3.x you can also use the alias `String` for this validator.
+ You can also use the alias `String` for this validator.
In addition to the String arguments, an encoding argument is also
- accepted. By default the encoding will be utf-8. You can overwrite
+ accepted. By default, the encoding will be utf-8. You can overwrite
this using the encoding parameter. You can also set inputEncoding
and outputEncoding differently. An inputEncoding of None means
"do not decode", an outputEncoding of None means "do not encode".
@@ -1129,11 +1100,11 @@ class UnicodeString(ByteString):
::
- >>> UnicodeString().to_python(None) == u''
+ >>> UnicodeString().to_python(None) == ''
True
- >>> UnicodeString().to_python([]) == u''
+ >>> UnicodeString().to_python([]) == ''
True
- >>> UnicodeString(encoding='utf-7').to_python('Ni Ni Ni') == u'Ni Ni Ni'
+ >>> UnicodeString(encoding='utf-7').to_python('Ni Ni Ni') == 'Ni Ni Ni'
True
"""
@@ -1152,44 +1123,31 @@ def __init__(self, **kw):
def _convert_to_python(self, value, state):
if not value:
- return u''
- if isinstance(value, six.text_type):
+ return ''
+ if isinstance(value, str):
return value
- if not isinstance(value, six.text_type):
- if hasattr(value, '__unicode__'):
- value = six.text_type(value)
- return value
- if not (six.text_type is str # Python 3
- and isinstance(value, bytes) and self.inputEncoding):
- value = str(value)
- if self.inputEncoding and not isinstance(value, six.text_type):
+ if isinstance(value, bytes):
try:
- value = six.text_type(value, self.inputEncoding)
+ return value.decode(
+ encoding=self.inputEncoding or self.encoding)
except UnicodeDecodeError:
raise Invalid(self.message('badEncoding', state), value, state)
- except TypeError:
- raise Invalid(
- self.message('badType', state,
- type=type(value), value=value), value, state)
- return value
+ return str(value)
def _convert_from_python(self, value, state):
- if not isinstance(value, six.text_type):
- if hasattr(value, '__unicode__'):
- value = six.text_type(value)
- else:
- value = str(value)
- if self.outputEncoding and isinstance(value, six.text_type):
+ if not isinstance(value, str):
+ value = str(value)
+ if self.outputEncoding and isinstance(value, str):
value = value.encode(self.outputEncoding)
return value
def empty_value(self, value):
- return u''
+ return ''
# Provide proper alias for native strings
-String = UnicodeString if str is six.text_type else ByteString
+String = UnicodeString
class Set(FancyValidator):
@@ -1213,11 +1171,11 @@ class Set(FancyValidator):
['this', 'that']
>>> s = Set(use_set=True)
>>> s.to_python(None)
- set([])
+ set()
>>> s.to_python('this')
- set(['this'])
+ {'this'}
>>> s.to_python(('this',))
- set(['this'])
+ {'this'}
"""
use_set = False
@@ -1357,8 +1315,7 @@ def _validate_python(self, value, state):
value, state)
try:
idna_domain = [idna.ToASCII(p) for p in domain.split('.')]
- if six.text_type is str: # Python 3
- idna_domain = [p.decode('ascii') for p in idna_domain]
+ idna_domain = [p.decode('ascii') for p in idna_domain]
idna_domain = '.'.join(idna_domain)
except UnicodeError:
# UnicodeError: label empty or too long
@@ -1378,10 +1335,10 @@ def _validate_python(self, value, state):
import socket
try:
try:
- dns.resolver.query(domain, 'MX')
+ dns.resolver.resolve(domain, 'MX')
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
try:
- dns.resolver.query(domain, 'A')
+ dns.resolver.resolve(domain, 'A')
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
raise Invalid(
self.message('domainDoesNotExist',
@@ -1395,7 +1352,7 @@ def _convert_to_python(self, value, state):
class URL(FancyValidator):
- """
+ r"""
Validate a URL, either http://... or https://. If check_exists
is true, then we'll actually make a request for the page.
@@ -1440,7 +1397,7 @@ class URL(FancyValidator):
Traceback (most recent call last):
...
Invalid: You must start your URL with http://, https://, etc
- >>> u.to_python('http://www.formencode.org/does/not/exist/page.html')
+ >>> u.to_python('https://httpbin.org/status/404')
Traceback (most recent call last):
...
Invalid: The server responded that the page could not be found
@@ -1460,13 +1417,13 @@ class URL(FancyValidator):
You may set allow_idna to False to change this behavior::
>>> URL(allow_idna=True).to_python(
- ... u'http://\u0433\u0443\u0433\u043b.\u0440\u0444')
+ ... 'http://\u0433\u0443\u0433\u043b.\u0440\u0444')
'http://xn--c1aay4a.xn--p1ai'
>>> URL(allow_idna=True, add_http=True).to_python(
- ... u'\u0433\u0443\u0433\u043b.\u0440\u0444')
+ ... '\u0433\u0443\u0433\u043b.\u0440\u0444')
'http://xn--c1aay4a.xn--p1ai'
>>> URL(allow_idna=False).to_python(
- ... u'http://\u0433\u0443\u0433\u043b.\u0440\u0444')
+ ... 'http://\u0433\u0443\u0433\u043b.\u0440\u0444')
Traceback (most recent call last):
...
Invalid: That is not a valid URL
@@ -1530,7 +1487,7 @@ def _convert_to_python(self, value, state):
def _encode_idna(self, url):
global urlparse
if urlparse is None:
- from six.moves.urllib import parse as urlparse
+ from urllib import parse as urlparse
try:
scheme, netloc, path, params, query, fragment = urlparse.urlparse(
url)
@@ -1538,8 +1495,7 @@ def _encode_idna(self, url):
return url
try:
netloc = netloc.encode('idna')
- if six.text_type is str: # Python 3
- netloc = netloc.decode('ascii')
+ netloc = netloc.decode('ascii')
return str(urlparse.urlunparse((scheme, netloc,
path, params, query, fragment)))
except UnicodeError:
@@ -1548,9 +1504,9 @@ def _encode_idna(self, url):
def _check_url_exists(self, url, state):
global http_client, urlparse, socket
if http_client is None:
- from six.moves import http_client
+ from http import client as http_client
if urlparse is None:
- from six.moves.urllib import parse as urlparse
+ import parse as urlparse
if socket is None:
import socket
scheme, netloc, path, params, query, fragment = urlparse.urlparse(
@@ -1569,28 +1525,13 @@ def _check_url_exists(self, url, state):
conn.close()
except http_client.HTTPException as e:
e = str(e)
- if str is not six.text_type: # Python 2
- try:
- e = e.decode('utf-8')
- except UnicodeDecodeError:
- try:
- e = e.decode('latin-1')
- except UnicodeDecodeError:
- e = e.decode('ascii', 'replace')
raise Invalid(
- self.message('httpError', state, error=e), state, url)
+ self.message('httpError', state, error=str(e)),
+ state, url)
except socket.error as e:
- e = str(e)
- if str is not six.text_type: # Python 2
- try:
- e = e.decode('utf-8')
- except UnicodeDecodeError:
- try:
- e = e.decode('latin-1')
- except UnicodeDecodeError:
- e = e.decode('ascii', 'replace')
raise Invalid(
- self.message('socketError', state, error=e), state, url)
+ self.message('socketError', state, error=str(e)),
+ state, url)
else:
if res.status == 404:
raise Invalid(
@@ -1710,7 +1651,7 @@ def _validate_python(self, value, state=None):
is not valid.
"""
- if not isinstance(value, six.string_types):
+ if not isinstance(value, str):
raise Invalid(
self.message('badType', state,
type=str(type(value)), value=value), value, state)
@@ -1719,7 +1660,7 @@ def _validate_python(self, value, state=None):
if value.startswith('xri://'):
value = value[6:]
- if not value[0] in ('@', '=') and not (
+ if value[0] not in ('@', '=') and not (
self.xri_type == 'i-number' and value[0] == '!'):
raise Invalid(self.message('noType', state), value, state)
@@ -1832,9 +1773,21 @@ class FieldStorageUploadConverter(FancyValidator):
This doesn't do any conversion, but it can detect empty upload
fields (which appear like normal fields, but have no filename when
no upload was given).
+
+ Requires the legacy-cgi package on Python 3.13 and later.
"""
+
+ def __init__(self, *args, **kw):
+ if cgi is None:
+ warnings.warn(
+ "legacy-cgi is not"
+ " installed on your system (or the cgi package cannot be"
+ " found). I cannot convert FieldStorage")
+ raise ImportError("no module named cgi")
+ super().__init__(*args, **kw)
+
def _convert_to_python(self, value, state=None):
- if isinstance(value, cgi.FieldStorage):
+ if cgi and isinstance(value, cgi.FieldStorage):
if getattr(value, 'filename', None):
return value
raise Invalid('invalid', value, state)
@@ -1842,7 +1795,7 @@ def _convert_to_python(self, value, state=None):
return value
def is_empty(self, value):
- if isinstance(value, cgi.FieldStorage):
+ if cgi and isinstance(value, cgi.FieldStorage):
return not bool(getattr(value, 'filename', None))
return FancyValidator.is_empty(self, value)
@@ -1878,6 +1831,9 @@ class MyScheme(Scheme):
Note that big file uploads mean big hidden fields, and lots of
bytes passed back and forth in the case of an error.
+
+ Note: requires the legacy-cgi package on Python 3.13 and later to be able to handle
+ ``cgi.FieldStorage`` values.
"""
upload_key = 'upload'
@@ -1887,10 +1843,10 @@ def _convert_to_python(self, value, state):
upload = value.get(self.upload_key)
static = value.get(self.static_key, '').strip()
filename = content = None
- if isinstance(upload, cgi.FieldStorage):
+ if cgi and isinstance(upload, cgi.FieldStorage):
filename = upload.filename
content = upload.value
- elif isinstance(upload, six.string_types) and upload:
+ elif isinstance(upload, str) and upload:
filename = None
# @@: Should this encode upload if it is unicode?
content = upload
@@ -1998,19 +1954,19 @@ class DateConverter(FancyValidator):
_date_re = dict(
dmy=re.compile(
r'^\s*(\d\d?)[\-\./\\](\d\d?|%s)[\-\./\\](\d\d\d?\d?)\s*$'
- % '|'.join(_month_names), re.I),
+ % '|'.join(_month_names), re.I),
mdy=re.compile(
r'^\s*(\d\d?|%s)[\-\./\\](\d\d?)[\-\./\\](\d\d\d?\d?)\s*$'
- % '|'.join(_month_names), re.I),
+ % '|'.join(_month_names), re.I),
ymd=re.compile(
r'^\s*(\d\d\d?\d?)[\-\./\\](\d\d?|%s)[\-\./\\](\d\d?)\s*$'
- % '|'.join(_month_names), re.I),
+ % '|'.join(_month_names), re.I),
my=re.compile(
r'^\s*(\d\d?|%s)[\-\./\\](\d\d\d?\d?)\s*$'
- % '|'.join(_month_names), re.I),
+ % '|'.join(_month_names), re.I),
ym=re.compile(
r'^\s*(\d\d\d?\d?)[\-\./\\](\d\d?|%s)\s*$'
- % '|'.join(_month_names), re.I))
+ % '|'.join(_month_names), re.I))
_formats = dict(d='%d', m='%m', y='%Y')
@@ -2307,7 +2263,7 @@ def _to_python_tuple(self, value, state):
return hour, minute, second
def _convert_from_python(self, value, state):
- if isinstance(value, six.string_types):
+ if isinstance(value, str):
return value
if hasattr(value, 'hour'):
hour, minute = value.hour, value.minute
@@ -2334,8 +2290,6 @@ def _convert_from_python(self, value, state):
return '%i:%02i%s' % (hour, minute, ampm)
-
-
class ISODateTimeConverter(FancyValidator):
"""
Converts fields which contain both date and time, in the
@@ -2376,7 +2330,6 @@ def _convert_from_python(self, value, state):
return value.isoformat("T")
-
def PostalCode(*kw, **kwargs):
deprecation_warning("please use formencode.national.USPostalCode")
from formencode.national import USPostalCode
@@ -2448,7 +2401,7 @@ class StringBool(FancyValidator): # originally from TurboGears 1
string=_('Value should be %(true)r or %(false)r'))
def _convert_to_python(self, value, state):
- if isinstance(value, six.string_types):
+ if isinstance(value, str):
value = value.strip().lower()
if value in self.true_values:
return True
@@ -2463,6 +2416,7 @@ def _convert_to_python(self, value, state):
def _convert_from_python(self, value, state):
return (self.true_values if value else self.false_values)[0]
+
# Should deprecate:
StringBoolean = StringBool
@@ -2765,11 +2719,13 @@ def _convert_to_python(self, value_dict, state):
raise Invalid(
_('You must give a value for %s') % self.required,
value_dict, state,
- error_dict={self.required:
- Invalid(self.message('empty', state),
+ error_dict={
+ self.required: Invalid(
+ self.message('empty', state),
value_dict.get(self.required), state)})
return value_dict
+
RequireIfPresent = RequireIfMissing
@@ -2878,9 +2834,9 @@ def _validate_python(self, field_dict, state):
else:
errors[name] = self.message('invalidNoMatch', state)
if errors:
- error_list = sorted(six.iteritems(errors))
error_message = ' \n'.join(
- '%s: %s' % (name, value) for name, value in error_list)
+ '%s: %s' % (name, value)
+ for name, value in sorted(errors.items()))
raise Invalid(error_message, field_dict, state, error_dict=errors)
@@ -2937,10 +2893,9 @@ def validate_partial(self, field_dict, state):
def _validate_python(self, field_dict, state):
errors = self._validateReturn(field_dict, state)
if errors:
- error_list = sorted(six.iteritems(errors))
raise Invalid(
' \n'.join('%s: %s' % (name, value)
- for name, value in error_list),
+ for name, value in sorted(errors.items())),
field_dict, state, error_dict=errors)
def _validateReturn(self, field_dict, state):
@@ -3052,10 +3007,9 @@ def validate_partial(self, field_dict, state):
def _validate_python(self, field_dict, state):
errors = self._validateReturn(field_dict, state)
if errors:
- error_list = sorted(six.iteritems(errors))
raise Invalid(
' \n'.join('%s: %s' % (name, value)
- for name, value in error_list),
+ for name, value in sorted(errors.items())),
field_dict, state, error_dict=errors)
def _validateReturn(self, field_dict, state):
@@ -3076,15 +3030,17 @@ def _validateReturn(self, field_dict, state):
dt_mod, next_month_year, next_month, 1)
assert expires_date > today
except ValueError:
- return {self.cc_expires_month_field:
- self.message('notANumber', state),
- self.cc_expires_year_field:
- self.message('notANumber', state)}
+ return {
+ self.cc_expires_month_field:
+ self.message('notANumber', state),
+ self.cc_expires_year_field:
+ self.message('notANumber', state)}
except AssertionError:
- return {self.cc_expires_month_field:
- self.message('invalidNumber', state),
- self.cc_expires_year_field:
- self.message('invalidNumber', state)}
+ return {
+ self.cc_expires_month_field:
+ self.message('invalidNumber', state),
+ self.cc_expires_year_field:
+ self.message('invalidNumber', state)}
class CreditCardSecurityCode(FormValidator):
@@ -3126,11 +3082,10 @@ def validate_partial(self, field_dict, state):
def _validate_python(self, field_dict, state):
errors = self._validateReturn(field_dict, state)
if errors:
- error_list = sorted(six.iteritems(errors))
raise Invalid(
' \n'.join(
'%s: %s' % (name, value)
- for name, value in error_list),
+ for name, value in errors.items()),
field_dict, state, error_dict=errors)
def _validateReturn(self, field_dict, state):
@@ -3150,7 +3105,8 @@ def _validateReturn(self, field_dict, state):
def validators():
"""Return the names of all validators in this module."""
- return [name for name, value in six.iteritems(globals())
- if isinstance(value, type) and issubclass(value, Validator)]
+ return [name for name, value in globals().items()
+ if isinstance(value, type) and issubclass(value, Validator)]
+
__all__ = ['Invalid'] + validators()
diff --git a/formencode/variabledecode.py b/src/formencode/variabledecode.py
similarity index 94%
rename from formencode/variabledecode.py
rename to src/formencode/variabledecode.py
index 7f93416c..4fe9a1ce 100644
--- a/formencode/variabledecode.py
+++ b/src/formencode/variabledecode.py
@@ -19,11 +19,8 @@
and list_char keyword args. For example, to have the GET/POST variables,
``a_1=something`` as a list, you would use a ``list_char='_'``.
"""
-from __future__ import absolute_import
from .api import FancyValidator
-import six
-from six.moves import range
__all__ = ['variable_decode', 'variable_encode', 'NestedVariables']
@@ -31,7 +28,7 @@
def _sort_key(item):
"""Robust sort key that sorts items with invalid keys last.
- This is used to make sorting behave the same across Python 2 and 3.
+ This is used to make sorting work with keys that have difference types.
"""
key = item[0]
return not isinstance(key, int), key
@@ -42,7 +39,7 @@ def variable_decode(d, dict_char='.', list_char='-'):
result = {}
dicts_to_sort = set()
known_lengths = {}
- for key, value in six.iteritems(d):
+ for key, value in d.items():
keys = key.split(dict_char)
new_keys = []
was_repetition_count = False
@@ -104,10 +101,10 @@ def variable_decode(d, dict_char='.', list_char='-'):
to_sort = to_sort[sub_key]
if None in to_sort:
none_values = [(0, x) for x in to_sort.pop(None)]
- none_values.extend(six.iteritems(to_sort))
+ none_values.extend(to_sort.items())
to_sort = none_values
else:
- to_sort = six.iteritems(to_sort)
+ to_sort = to_sort.items()
to_sort = [x[1] for x in sorted(to_sort, key=_sort_key)]
if key in known_lengths:
if len(to_sort) < known_lengths[key]:
@@ -123,7 +120,7 @@ def variable_encode(d, prepend='', result=None, add_repetitions=True,
if result is None:
result = {}
if isinstance(d, dict):
- for key, value in six.iteritems(d):
+ for key, value in d.items():
if key is None:
name = prepend
elif not prepend:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..922f3c1a
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,26 @@
+import os
+import sys
+import warnings
+
+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 (which are disabled by default)
+warnings.simplefilter('default')
+
+try:
+ try:
+ import importlib.metadata as metadata
+ try:
+ metadata.distribution("FormEncode")
+ except metadata.PackageNotFoundError as error:
+ raise ImportError from error
+ except ImportError: # Python < 3.8
+ import pkg_resources
+ pkg_resources.require('FormEncode')
+except ImportError as error:
+ raise ImportError("Install FormEncode before running the tests") from error
diff --git a/formencode/tests/htmlfill_data/data-error1.txt b/tests/htmlfill_data/data-error1.txt
similarity index 100%
rename from formencode/tests/htmlfill_data/data-error1.txt
rename to tests/htmlfill_data/data-error1.txt
diff --git a/formencode/tests/htmlfill_data/data-fill1.txt b/tests/htmlfill_data/data-fill1.txt
similarity index 100%
rename from formencode/tests/htmlfill_data/data-fill1.txt
rename to tests/htmlfill_data/data-fill1.txt
diff --git a/formencode/tests/htmlfill_data/data-fill2.txt b/tests/htmlfill_data/data-fill2.txt
similarity index 100%
rename from formencode/tests/htmlfill_data/data-fill2.txt
rename to tests/htmlfill_data/data-fill2.txt
diff --git a/formencode/tests/htmlfill_data/data-fill3.txt b/tests/htmlfill_data/data-fill3.txt
similarity index 100%
rename from formencode/tests/htmlfill_data/data-fill3.txt
rename to tests/htmlfill_data/data-fill3.txt
diff --git a/formencode/tests/htmlfill_data/data-fill4.txt b/tests/htmlfill_data/data-fill4.txt
similarity index 100%
rename from formencode/tests/htmlfill_data/data-fill4.txt
rename to tests/htmlfill_data/data-fill4.txt
diff --git a/formencode/tests/htmlfill_data/data-form-last-element.txt b/tests/htmlfill_data/data-form-last-element.txt
similarity index 100%
rename from formencode/tests/htmlfill_data/data-form-last-element.txt
rename to tests/htmlfill_data/data-form-last-element.txt
diff --git a/formencode/tests/htmlfill_data/data-schema1.txt b/tests/htmlfill_data/data-schema1.txt
similarity index 100%
rename from formencode/tests/htmlfill_data/data-schema1.txt
rename to tests/htmlfill_data/data-schema1.txt
diff --git a/formencode/tests/non_empty.txt b/tests/non_empty.txt
similarity index 95%
rename from formencode/tests/non_empty.txt
rename to tests/non_empty.txt
index 8db8ec1d..bbed33e6 100644
--- a/formencode/tests/non_empty.txt
+++ b/tests/non_empty.txt
@@ -1,6 +1,4 @@
michele@ionic:~/Progetti/TurboGears/svn/thirdparty/formencode/formencode$ python
-Python 2.4.3 (#2, Mar 30 2006, 14:45:01)
-[GCC 4.0.3 (Ubuntu 4.0.3-1ubuntu3)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from formencode import validators
>>> int = validators.Int()
diff --git a/formencode/tests/test_cc.py b/tests/test_cc.py
similarity index 90%
rename from formencode/tests/test_cc.py
rename to tests/test_cc.py
index c0e5319b..429ba114 100644
--- a/formencode/tests/test_cc.py
+++ b/tests/test_cc.py
@@ -1,6 +1,3 @@
-from __future__ import absolute_import
-# -*- coding: utf-8 -*-
-
import unittest
from formencode import Invalid
@@ -24,7 +21,7 @@ def message(self, key):
def test_validate(self):
validate, message = self.validate, self.message
- self.assertTrue(validate('visa', '4' + '1' * 15) is None)
+ self.assertIsNone(validate('visa', '4' + '1' * 15))
self.assertEqual(validate('visa', '5' + '1' * 12),
message('invalidNumber'))
self.assertEqual(validate('visa', '4' + '1' * 11 + '2'),
@@ -52,7 +49,7 @@ def message(self, key):
def test_validate(self):
validate, message = self.validate, self.message
- self.assertTrue(validate('11', '2250') is None)
+ self.assertIsNone(validate('11', '2250'))
self.assertEqual(validate('11', 'test'), message('notANumber'))
self.assertEqual(validate('test', '2250'), message('notANumber'))
self.assertEqual(validate('10', '2005'), message('invalidNumber'))
diff --git a/formencode/tests/test_compound.py b/tests/test_compound.py
similarity index 88%
rename from formencode/tests/test_compound.py
rename to tests/test_compound.py
index d74f8d61..c8f838ea 100644
--- a/formencode/tests/test_compound.py
+++ b/tests/test_compound.py
@@ -1,6 +1,3 @@
-from __future__ import absolute_import
-# -*- coding: utf-8 -*-
-
import unittest
from formencode import compound, Invalid
@@ -14,8 +11,8 @@ def setUp(self):
def test_repr(self):
r = repr(self.validator)
- self.assertFalse('validatorArgs' in r)
- self.assertTrue('validators=[]' in r)
+ self.assertNotIn('validatorArgs', r)
+ self.assertIn('validators=[]', r)
def test_to_python(self):
self.assertRaises(NotImplementedError,
@@ -38,7 +35,7 @@ def setUp(self):
def test_repr(self):
r = repr(self.validator)
- self.assertFalse('validatorArgs' in r)
+ self.assertNotIn('validatorArgs', r)
self.assertEqual(r.count('DictConverter'), 2)
def test_to_python(self):
@@ -61,7 +58,7 @@ def setUp(self):
def test_repr(self):
r = repr(self.validator)
- self.assertFalse('validatorArgs' in r)
+ self.assertNotIn('validatorArgs', r)
self.assertEqual(r.count('DictConverter'), 3)
def test_to_python(self):
@@ -76,7 +73,7 @@ def test_to_python_error(self):
try:
self.validator.to_python(3)
except Invalid as e:
- self.assertTrue('Enter a value from: 2' in str(e))
+ self.assertIn('Enter a value from: 2', str(e))
else:
self.fail('Invalid should be raised when no validator succeeds.')
@@ -93,7 +90,7 @@ def setUp(self):
def test_repr(self):
r = repr(self.validator)
- self.assertFalse('validatorArgs' in r)
+ self.assertNotIn('validatorArgs', r)
self.assertEqual(r.count('DictConverter'), 2)
def test_to_python(self):
diff --git a/formencode/tests/test_context.py b/tests/test_context.py
similarity index 69%
rename from formencode/tests/test_context.py
rename to tests/test_context.py
index 5fcd52d5..34da8fa5 100644
--- a/formencode/tests/test_context.py
+++ b/tests/test_context.py
@@ -1,5 +1,4 @@
-from __future__ import absolute_import
-from nose.tools import assert_raises
+import pytest
from formencode.context import Context, ContextRestoreError
@@ -34,7 +33,8 @@ def test_fail():
c3 = Context()
res1 = c3.set(a=1)
res2 = c3.set(b=2)
- assert_raises(ContextRestoreError, res1.restore)
+ with pytest.raises(ContextRestoreError):
+ res1.restore()
assert c3.b == 2
assert c3.a == 1
res2.restore()
@@ -53,3 +53,16 @@ def test_default():
assert con.a == 2
res.restore()
assert con.a == 4
+
+
+def test_context_manager():
+ with c1.set(foo=1):
+ assert_is(c1, 'foo', 1)
+ assert_is(c1, 'foo', None)
+ with c1.set(foo=2), c2.set(foo='test'):
+ assert_is(c1, 'foo', 2)
+ assert_is(c2, 'foo', 'test')
+ change_state(c1, assert_is, c1, 'foo', 3, foo=3)
+ assert_is(c1, 'foo', 2)
+ assert_is(c1, 'foo', None)
+ assert not hasattr(c2, 'foo')
diff --git a/formencode/tests/test_declarative.py b/tests/test_declarative.py
similarity index 88%
rename from formencode/tests/test_declarative.py
rename to tests/test_declarative.py
index ffce388a..1217621c 100644
--- a/formencode/tests/test_declarative.py
+++ b/tests/test_declarative.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-# -*- coding: utf-8 -*-
import unittest
import re
@@ -24,8 +22,8 @@ def test_call(self):
D = declarative.Declarative
obj_bar = D(foo='bar', woo='par')
obj_baz = obj_bar(foo='baz')
- self.assertTrue(type(obj_bar) is type(obj_baz))
- self.assertTrue(obj_bar is not obj_baz)
+ self.assertIs(type(obj_bar), type(obj_baz))
+ self.assertIsNot(obj_bar, obj_baz)
self.assertEqual(obj_baz.foo, 'baz')
self.assertEqual(obj_baz.woo, 'par')
diff --git a/formencode/tests/test_doctest_xml_compare.py b/tests/test_doctest_xml_compare.py
similarity index 90%
rename from formencode/tests/test_doctest_xml_compare.py
rename to tests/test_doctest_xml_compare.py
index 316abac1..b6fa5e56 100644
--- a/formencode/tests/test_doctest_xml_compare.py
+++ b/tests/test_doctest_xml_compare.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
import sys
import formencode.doctest_xml_compare as dxml
diff --git a/formencode/tests/test_doctests.py b/tests/test_doctests.py
similarity index 52%
rename from formencode/tests/test_doctests.py
rename to tests/test_doctests.py
index 800eb04b..b02d22db 100644
--- a/formencode/tests/test_doctests.py
+++ b/tests/test_doctests.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
import os
import sys
import doctest
@@ -9,7 +8,8 @@
from formencode import national
from formencode import schema
from formencode import validators
-import six
+
+import pytest
"""Modules that will have their doctests tested."""
@@ -18,64 +18,50 @@
"""Text files that will have their doctests tested."""
text_files = [
- 'docs/htmlfill.txt',
- 'docs/Validator.txt',
- 'formencode/tests/non_empty.txt',
- ]
+ "docs/htmlfill.txt",
+ "docs/Validator.txt",
+ "tests/non_empty.txt",
+]
"""Used to resolve text files to absolute paths."""
-base = os.path.dirname(os.path.dirname(os.path.dirname(
- os.path.abspath(__file__))))
-
-
-if six.text_type is str: # Python 3
-
- OutputChecker = doctest.OutputChecker
-
- class OutputChecker3(OutputChecker):
-
- def check_output(self, want, got, optionflags):
- if want.startswith("u'"):
- want = want[1:]
- elif want.startswith('set(['):
- want = want[3:].replace(
- '([', '{').replace('])', '}').replace('{}', 'set()')
- return OutputChecker.check_output(self, want, got, optionflags)
-
- doctest.OutputChecker = OutputChecker3
+base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def doctest_file(document, verbose, raise_error):
- failure_count, test_count = doctest.testfile(document,
- module_relative=False,
- optionflags=doctest.ELLIPSIS | doctest.IGNORE_EXCEPTION_DETAIL,
- verbose=verbose)
+ failure_count, test_count = doctest.testfile(
+ document,
+ module_relative=False,
+ optionflags=doctest.ELLIPSIS | doctest.IGNORE_EXCEPTION_DETAIL,
+ verbose=verbose,
+ )
if raise_error:
assert test_count > 0
assert failure_count == 0
def doctest_module(document, verbose, raise_error):
- failure_count, test_count = doctest.testmod(document,
- optionflags=doctest.ELLIPSIS | doctest.IGNORE_EXCEPTION_DETAIL,
- verbose=verbose)
+ failure_count, test_count = doctest.testmod(
+ document,
+ optionflags=doctest.ELLIPSIS | doctest.IGNORE_EXCEPTION_DETAIL,
+ verbose=verbose,
+ )
if raise_error:
assert test_count > 0
assert failure_count == 0
def set_func_description(fn, description):
- """Wrap function and set description attr for nosetests to display."""
+ """Wrap function and set description attr for pytest to display."""
+
def _wrapper(*a_test_args):
fn(*a_test_args)
+
_wrapper.description = description
return _wrapper
-def test_doctests():
- """Generate each doctest."""
- # TODO Can we resolve this from nose?
+def collect_functions():
verbose = False
raise_error = True
for document in text_files + modules:
@@ -83,20 +69,30 @@ def test_doctests():
name = "Doctests for %s" % (document,)
if not document.startswith(os.sep):
document = os.path.join(base, document)
- yield set_func_description(doctest_file, name), document, \
- verbose, raise_error
+ yield set_func_description(
+ doctest_file, name
+ ), document, verbose, raise_error
else:
name = "Doctests for %s" % (document.__name__,)
- yield set_func_description(doctest_module, name), document, \
- verbose, raise_error
+ yield set_func_description(
+ doctest_module, name
+ ), document, verbose, raise_error
+
+
+@pytest.mark.parametrize(
+ "testfn,document,verbose,raise_error", list(collect_functions())
+)
+def test_doctests(testfn, document, verbose, raise_error):
+ """Generate each doctest."""
+ testfn(document, verbose, raise_error)
-if __name__ == '__main__':
+if __name__ == "__main__":
# Call this file directly if you want to test doctests.
args = sys.argv[1:]
verbose = False
- if '-v' in args:
- args.remove('-v')
+ if "-v" in args:
+ args.remove("-v")
verbose = True
if not args:
args = text_files + modules
diff --git a/formencode/tests/test_email.py b/tests/test_email.py
similarity index 87%
rename from formencode/tests/test_email.py
rename to tests/test_email.py
index f79aea80..ac40b7a6 100644
--- a/formencode/tests/test_email.py
+++ b/tests/test_email.py
@@ -1,12 +1,8 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-from __future__ import unicode_literals
import unittest
from formencode import Invalid
from formencode.validators import Email
-import six
class TestEmail(unittest.TestCase):
@@ -18,7 +14,7 @@ def validate(self, *args):
try:
return self.validator.to_python(*args)
except Invalid as e:
- return six.text_type(e)
+ return str(e)
def message(self, message_name, username, domain):
email = '@'.join((username, domain))
@@ -56,10 +52,10 @@ def test_valid_email_addresses(self):
('foo{bar}@example.com', 'foo{bar}@example.com'),
# examples from RFC 3696
# punting on the difficult and extremely uncommon ones
- #('"Abc\@def"@example.com', '"Abc\@def"@example.com'),
- #('"Fred Bloggs"@example.com', '"Fred Bloggs"@example.com'),
- #('"Joe\\Blow"@example.com', '"Joe\\Blow"@example.com'),
- #('"Abc@def"@example.com', '"Abc@def"@example.com'),
+ # ('"Abc\@def"@example.com', '"Abc\@def"@example.com'),
+ # ('"Fred Bloggs"@example.com', '"Fred Bloggs"@example.com'),
+ # ('"Joe\\Blow"@example.com', '"Joe\\Blow"@example.com'),
+ # ('"Abc@def"@example.com', '"Abc@def"@example.com'),
('customer/department=shipping@example.com',
'customer/department=shipping@example.com'),
('$A12345@example.com', '$A12345@example.com'),
diff --git a/formencode/tests/test_htmlfill.py b/tests/test_htmlfill.py
similarity index 97%
rename from formencode/tests/test_htmlfill.py
rename to tests/test_htmlfill.py
index 431e03bf..5e575517 100644
--- a/formencode/tests/test_htmlfill.py
+++ b/tests/test_htmlfill.py
@@ -1,20 +1,12 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-from __future__ import print_function
-from __future__ import unicode_literals
-
import os
import re
import sys
+from html.entities import name2codepoint
+
import xml.etree.ElementTree as ET
-import six
-try:
- XMLParseError = ET.ParseError
-except AttributeError: # Python < 2.7
- from xml.parsers.expat import ExpatError as XMLParseError
-from six.moves.html_entities import name2codepoint
+XMLParseError = ET.ParseError
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__)))))
@@ -24,16 +16,19 @@
from formencode import htmlfill, htmlfill_schemabuilder
from formencode.doctest_xml_compare import xml_compare
+import pytest
+
-def test_inputoutput():
+def collect_values():
data_dir = os.path.join(os.path.dirname(__file__), 'htmlfill_data')
for fn in os.listdir(data_dir):
if fn.startswith('data-'):
fn = os.path.join(data_dir, fn)
- yield run_filename, fn
+ yield fn
-def run_filename(filename):
+@pytest.mark.parametrize('filename', list(collect_values()))
+def test_runfile(filename):
f = open(filename)
content = f.read()
f.close()
@@ -48,7 +43,7 @@ def run_filename(filename):
data_content = ''
namespace = {}
if data_content:
- six.exec_(data_content, namespace)
+ exec(data_content, namespace)
data = namespace.copy()
data['defaults'] = data.get('defaults', {})
if 'check' in data:
@@ -91,7 +86,7 @@ def test_no_trailing_newline():
def test_escape_defaults():
- rarr = six.unichr(name2codepoint['rarr'])
+ rarr = chr(name2codepoint['rarr'])
assert (htmlfill.render('', {}, {})
== '' % rarr)
assert (htmlfill.render('', {}, {})
@@ -459,7 +454,7 @@ def test_error_class_textarea():
def test_mix_str_and_unicode():
html = ''
- uhtml = six.text_type(html)
+ uhtml = str(html)
cheese = dict(cheese='Käse')
ucheese = dict(cheese='Käse')
expected = ''
diff --git a/formencode/tests/test_htmlfill_control.py b/tests/test_htmlfill_control.py
similarity index 99%
rename from formencode/tests/test_htmlfill_control.py
rename to tests/test_htmlfill_control.py
index 4ee4fa6c..e6959594 100644
--- a/formencode/tests/test_htmlfill_control.py
+++ b/tests/test_htmlfill_control.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
from formencode import htmlfill
@@ -95,6 +94,7 @@ def test_error_attr_ignore():
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():
@@ -175,4 +175,3 @@ def test_error_attr_form_alt():
test_error_attr_ignore()
test_error_attr_form()
test_error_attr_form_alt()
-
\ No newline at end of file
diff --git a/formencode/tests/test_htmlgen.py b/tests/test_htmlgen.py
similarity index 84%
rename from formencode/tests/test_htmlgen.py
rename to tests/test_htmlgen.py
index 9dccf5bf..0af2421b 100644
--- a/formencode/tests/test_htmlgen.py
+++ b/tests/test_htmlgen.py
@@ -1,14 +1,9 @@
-from __future__ import absolute_import
-from __future__ import print_function
-
import doctest
from formencode.htmlgen import html
-import six
# A test value that can't be encoded as ascii:
-uni_value = u'\xff'
-str_value = uni_value if str is six.text_type else uni_value.encode('utf-8')
+uni_value = '\xff'
def test_basic():
@@ -40,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 six.text_type(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>'
@@ -61,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')) == ''
@@ -79,7 +74,7 @@ def test_namespace():
if __name__ == '__main__':
# It's like a super-mini py.test...
- for name, value in six.iteritems(globals()):
+ for name, value in globals().items():
if name.startswith('test'):
print(name)
value()
diff --git a/formencode/tests/test_htmlrename.py b/tests/test_htmlrename.py
similarity index 95%
rename from formencode/tests/test_htmlrename.py
rename to tests/test_htmlrename.py
index 5f93c4a7..86a6f92d 100644
--- a/formencode/tests/test_htmlrename.py
+++ b/tests/test_htmlrename.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
from formencode.htmlrename import rename, add_prefix
diff --git a/formencode/tests/test_i18n.py b/tests/test_i18n.py
similarity index 89%
rename from formencode/tests/test_i18n.py
rename to tests/test_i18n.py
index 5998f96b..77ad988a 100644
--- a/formencode/tests/test_i18n.py
+++ b/tests/test_i18n.py
@@ -1,9 +1,4 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
import formencode
-import six
ne = formencode.validators.NotEmpty()
@@ -11,15 +6,15 @@
def _test_builtins(func):
def dummy(s):
return "builtins dummy"
- import six.moves.builtins
- six.moves.builtins._ = dummy
+ import builtins
+ builtins._ = dummy
try:
ne.to_python("")
except formencode.api.Invalid as e:
func(e)
- del six.moves.builtins._
+ del builtins._
def test_builtins():
@@ -38,7 +33,7 @@ def withoutbuiltins(e):
def test_state():
- class st(object):
+ class st:
def _(self, s):
return "state dummy"
@@ -55,7 +50,7 @@ def _test_lang(language, notemptytext):
try:
ne.to_python("")
except formencode.api.Invalid as e:
- assert six.text_type(e) == notemptytext
+ assert str(e) == notemptytext
formencode.api.set_stdtranslation() # set back to defaults
diff --git a/formencode/tests/test_schema.py b/tests/test_schema.py
similarity index 92%
rename from formencode/tests/test_schema.py
rename to tests/test_schema.py
index 56b153cb..4f57bb38 100644
--- a/formencode/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1,13 +1,13 @@
-from __future__ import absolute_import
-from __future__ import print_function
import unittest
-from six.moves.urllib.parse 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
@@ -16,14 +16,14 @@ def _notranslation(s):
def setup_module(module):
"""Disable i18n translation"""
- import six.moves.builtins
- six.moves.builtins._ = _notranslation
+ import builtins
+ builtins._ = _notranslation
def teardown_module(module):
"""Remove translation function"""
- import six.moves.builtins
- del six.moves.builtins._
+ import builtins
+ del builtins._
def d(**kw):
@@ -44,16 +44,15 @@ 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)
@@ -74,7 +73,7 @@ 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)))
except Invalid as e:
@@ -146,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():
@@ -172,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():
@@ -206,7 +210,7 @@ def f(value_dict, state, validator):
assert f.__doc__ == g.__doc__, "Docstrings don't match!"
-class State(object):
+class State:
pass
@@ -256,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):
@@ -276,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):
#
@@ -284,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 3f620797..ec6c2cbd 100644
--- a/formencode/tests/test_subclassing.py
+++ b/tests/test_subclassing.py
@@ -1,6 +1,3 @@
-from __future__ import absolute_import
-# -*- coding: utf-8 -*-
-
import unittest
from formencode.api import is_validator, FancyValidator, Invalid
@@ -50,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)
@@ -78,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')
@@ -93,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')
@@ -144,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)
@@ -159,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")
@@ -175,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)
@@ -244,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 88%
rename from formencode/tests/test_subclassing_old.py
rename to tests/test_subclassing_old.py
index 23b62df9..f47bd0de 100644
--- a/formencode/tests/test_subclassing_old.py
+++ b/tests/test_subclassing_old.py
@@ -1,14 +1,9 @@
-from __future__ import absolute_import
-# -*- coding: utf-8 -*-
-
import unittest
import warnings
from formencode.api import is_validator, FancyValidator, Invalid
from formencode.compound import CompoundValidator, All
from formencode.validators import Int
-from six.moves import map
-
with warnings.catch_warnings(record=True) as custom_warnings:
warnings.simplefilter('default')
@@ -54,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))
@@ -66,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)
@@ -94,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')
@@ -109,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')
@@ -160,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))
@@ -174,8 +162,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)
@@ -190,8 +177,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")
@@ -207,8 +193,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)
@@ -235,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)
@@ -283,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))
@@ -298,8 +283,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_validators.py b/tests/test_validators.py
similarity index 83%
rename from formencode/tests/test_validators.py
rename to tests/test_validators.py
index cc653192..5db4e085 100644
--- a/formencode/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -1,10 +1,6 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
import datetime
import unittest
-from nose.plugins.skip import SkipTest
+import pytest
from formencode import validators
from formencode.validators import Invalid
@@ -12,24 +8,18 @@
from formencode.schema import Schema
from formencode.foreach import ForEach
from formencode.api import NoDefault
-import six
-
-if not six.PY2:
- unicode = str
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()
@@ -39,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):
@@ -55,13 +45,12 @@ def setUp(self):
self.messages = self.validator.message
def test_alias(self):
- if str is not six.text_type: # 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)
@@ -93,56 +82,55 @@ def setUp(self):
self.validator = validators.UnicodeString
def test_alias(self):
- if str is six.text_type: # 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), '12')
- self.assertTrue(type(un.to_python(12)) is six.text_type)
+ self.assertIs(type(un.to_python(12)), str)
self.assertEqual(un.from_python(12), '12'.encode('ascii'))
- self.assertTrue(type(un.from_python(12)) is bytes)
+ self.assertIs(type(un.from_python(12)), bytes)
def test_unicode_encoding(self):
uv = self.validator()
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 six.text_type)
+ 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 six.text_type)
+ 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 six.text_type)
+ 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 six.text_type)
+ 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 six.text_type)
+ self.assertIs(type(uv.to_python(u8s)), str)
self.assertEqual(uv.from_python(us), us)
- self.assertTrue(type(uv.from_python(us)) is six.text_type)
+ self.assertIs(type(uv.from_python(us)), str)
def test_unicode_empty(self):
iv = self.validator()
for value in [None, b"", ""]:
result = iv.to_python(value)
self.assertEqual(result, "")
- self.assertTrue(isinstance(result, six.text_type))
+ self.assertIsInstance(result, str)
class TestIntValidator(unittest.TestCase):
@@ -153,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)
@@ -169,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))
@@ -198,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)
@@ -212,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 unicode(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 unicode(e))
+ self.assertIn(
+ 'Please enter the date in the form DD/MM/YYYY', str(e))
else:
self.fail('Date should be invalid')
@@ -237,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(
- unicode(e).replace("u'", "'") == "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(
- unicode(e).replace("u'", "'") == "Bad month_style: 'ydm'")
+ self.assertEqual(
+ str(e), "Bad month_style: 'ydm'")
else:
self.fail('month_style should be invalid')
@@ -261,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 unicode(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 unicode(e))
+ self.assertIn(
+ 'Please enter the date in the form MM/DD/YYYY', str(e))
else:
self.fail('Date should be invalid')
@@ -282,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 unicode(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 unicode(e))
+ self.assertIn(
+ 'Please enter the date in the form DD/MM/YYYY', str(e))
else:
self.fail('Date should be invalid')
@@ -303,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 unicode(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 unicode(e))
+ self.assertIn(
+ 'Please enter the date in the form YYYY/MM/DD', str(e))
else:
self.fail('Date should be invalid')
@@ -324,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 unicode(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 unicode(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 unicode(e))
+ self.assertIn(
+ 'Please enter the date in the form MM/YYYY', str(e))
else:
self.fail('Date should be invalid')
@@ -388,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 unicode(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 unicode(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 unicode(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 unicode(e))
- self.assertTrue('zx' in unicode(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')
@@ -621,8 +608,8 @@ 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(['ö', 'a'])
@@ -643,34 +630,27 @@ class foo(Schema):
self.assertEqual(expected, value)
def test_mixed_list(self):
- """before `_message_vars_decode`, the errors were:
- Value must be one of: a; b; ö; ™; 1; 2; 3 (not u'c')
- Value must be one of: a; b; ö; ™; 1; 2; 3 (not u'\xa7')
- """
o = validators.OneOf(['a', 'b', 'ö', '™', 1, 2, 3])
try:
o.to_python('c')
raise ValueError("exception expected")
except Invalid as e:
- self.assertTrue("Value must be one of: a; b; ö; ™; 1; 2; 3 (not 'c')" in unicode(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.assertTrue("Value must be one of: a; b; ö; ™; 1; 2; 3 (not 9)" in unicode(e))
+ self.assertIn("Value must be one of: a; b; ö; ™; 1; 2; 3 (not 9)", str(e))
try:
- o.to_python(u'd')
+ o.to_python('d')
raise ValueError("exception expected")
except Invalid as e:
- self.assertTrue("Value must be one of: a; b; ö; ™; 1; 2; 3 (not 'd')" in unicode(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:
- if six.PY2:
- self.assertTrue(r"Value must be one of: a; b; ö; ™; 1; 2; 3 (not u'\xa7')" in unicode(e))
- else:
- self.assertTrue(r"Value must be one of: a; b; ö; ™; 1; 2; 3 (not '§')" in unicode(e))
+ self.assertIn("Value must be one of: a; b; ö; ™; 1; 2; 3 (not '§')", str(e))
class TestIPAddressValidator(unittest.TestCase):
@@ -695,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 unicode(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 unicode(e))
+ self.assertIn('The octets must not have leading zeros', str(e))
else:
self.fail('IP octets with leading zeros should be invalid')
@@ -709,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 unicode(e).replace("u'", "'"))
+ 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')
@@ -727,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(
diff --git a/formencode/tests/test_variabledecode.py b/tests/test_variabledecode.py
similarity index 98%
rename from formencode/tests/test_variabledecode.py
rename to tests/test_variabledecode.py
index d7b6583b..6fe53f58 100644
--- a/formencode/tests/test_variabledecode.py
+++ b/tests/test_variabledecode.py
@@ -1,4 +1,3 @@
-from __future__ import absolute_import
import unittest
from formencode.variabledecode import variable_decode, variable_encode
diff --git a/tox.ini b/tox.ini
index 3b05face..64b7f69b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,13 +1,26 @@
[tox]
-envlist=py27,pypy,py36,py37,py38
+envlist=py3{7,8,9,10,11,12},pypy3,flake8,docs
+
+[testenv:docs]
+basepython = python3.11
+deps =
+ pycountry
+ sphinx
+commands =
+ sphinx-build -b html -nEW docs docs/_build/html
+
+[testenv:flake8]
+basepython = python3.11
+deps =
+ flake8
+commands =
+ flake8 src tests
[testenv]
deps=
- nose
+ pytest
wheel
- py27,pypy: pycountry < 19
- py27,pypy: dnspython < 2.0.0
- py{36,37,38}: pycountry dnspython
+ pycountry
+ dnspython >= 2
commands=
- python setup.py clean --all
- python setup.py nosetests
+ pytest tests {posargs}