diff --git a/.gitattributes b/.gitattributes
index 2c53400b..4aa54ca2 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -17,3 +17,5 @@
*.mo binary
*.png binary
+
+.git_archival.txt export-subst
diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml
new file mode 100644
index 00000000..7cb567a4
--- /dev/null
+++ b/.github/workflows/publish-to-pypi.yml
@@ -0,0 +1,88 @@
+name: Publish Python distribution to PyPI
+
+on: push
+
+jobs:
+ build:
+ name: Build distribution
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ fetch-depth: 0
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+ - name: Install pypa/build
+ run: python3 -m pip install build --user
+ - name: Build a binary wheel and a source tarball
+ run: python3 -m build
+ - name: Store the distribution packages
+ uses: actions/upload-artifact@v4
+ with:
+ name: python-package-distributions
+ path: dist/
+ publish-to-pypi:
+ name: Publish Python 🐍 distribution 📦 to PyPI
+ if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
+ needs:
+ - build
+ runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/p/FormEncode
+ permissions:
+ id-token: write # IMPORTANT: mandatory for trusted publishing
+ steps:
+ - name: Download all the dists
+ uses: actions/download-artifact@v4
+ with:
+ name: python-package-distributions
+ path: dist/
+ - name: Publish distribution to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ github-release:
+ name: >-
+ Sign the Python distribution with Sigstore
+ and upload them to GitHub Release
+ needs:
+ - publish-to-pypi
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: write # IMPORTANT: mandatory for making GitHub Releases
+ id-token: write # IMPORTANT: mandatory for sigstore
+
+ steps:
+ - name: Download all the dists
+ uses: actions/download-artifact@v4
+ with:
+ name: python-package-distributions
+ path: dist/
+ - name: Sign the dists with Sigstore
+ uses: sigstore/gh-action-sigstore-python@v3.0.0
+ with:
+ inputs: >-
+ ./dist/*.tar.gz
+ ./dist/*.whl
+ - name: Create GitHub Release
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: >-
+ gh release create
+ "$GITHUB_REF_NAME"
+ --repo "$GITHUB_REPOSITORY"
+ --notes ""
+ - name: Upload artifact signatures to GitHub Release
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ # Upload to GitHub Release using the `gh` CLI.
+ # `dist/` contains the built packages, and the
+ # sigstore-produced signatures and certificates.
+ run: >-
+ gh release upload
+ "$GITHUB_REF_NAME" dist/**
+ --repo "$GITHUB_REPOSITORY"
diff --git a/.github/workflows/run-qa.yml b/.github/workflows/run-qa.yml
new file mode 100644
index 00000000..0946cd2b
--- /dev/null
+++ b/.github/workflows/run-qa.yml
@@ -0,0 +1,22 @@
+name: Code quality
+
+on:
+ push:
+ branches:
+ - main
+ pull_request: {}
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.12'
+ - name: Install dependencies
+ run: pip install flake8
+ - name: Run code quality checks
+ run: make flake8
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
new file mode 100644
index 00000000..602ac332
--- /dev/null
+++ b/.github/workflows/run-tests.yml
@@ -0,0 +1,43 @@
+name: Tests
+
+on:
+ push:
+ branches:
+ - main
+ pull_request: {}
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 10
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
+ os: [ubuntu-latest, macOS-latest, windows-latest]
+ include:
+ # pypy3 on Windows currently fails trying to
+ # run dnspython tests. Moving pypy3 to only test linux.
+ - python-version: 'pypy3.10'
+ os: ubuntu-latest
+ - python-version: '3.7'
+ os: ubuntu-22.04
+ - python-version: '3.7'
+ os: macos-13
+ exclude:
+ - python-version: '3.7'
+ os: macOS-latest
+ - python-version: '3.7'
+ os: ubuntu-latest
+
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: make
+ - name: Run tests
+ run: make tests
diff --git a/.gitignore b/.gitignore
index 841a9115..619c6219 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,14 @@
*.egg
*.egg-info
+.eggs
+
*.pyc
*.pyo
-*.tox
+
+.tox
+
+.coverage
+coverage/
build
dist/
@@ -13,6 +19,4 @@ docs/_build
.project
.pydevproject
.settings
-
-.coverage
-coverage/
+.tool-versions
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..3dd84c58
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,22 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the version of Python and other tools you might need
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ configuration: docs/conf.py
+
+# We recommend specifying your dependencies to enable reproducible builds:
+# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
+python:
+ install:
+ - requirements: requirements-docs.txt
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 7cbe05a1..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-language: python
-
-python:
- - 2.6
- - 2.7
- - 3.2
- - 3.3
- - pypy
-
-install: python setup.py install
-
-script: python setup.py test
-
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
new file mode 100644
index 00000000..7be099e6
--- /dev/null
+++ b/CONTRIBUTORS.txt
@@ -0,0 +1,118 @@
+List of Contributors
+====================
+
+The below-signed are contributors to a code repository that is part of the
+project named "FormEncode". Each below-signed contributor has agreed to
+license their contributions under the MIT license (see LICENSE.txt) as of the
+date listed by their name.
+
+Contributors
+------------
+ - Ian Bicking, 2015-11-14
+
+ - Christoph Zwerschke, 2015-11-14
+
+ - Christopher Lambacher, 2015-11-14
+
+ - Philip Jenvey, 2015-11-14
+
+ - Ian Wilson, 2015-11-14
+
+ - W. Mark Kubacki, 2015-11-14
+
+ - Greg Horvath, 2015-11-14
+
+ - Maxim Avanov, 2015-11-14
+
+ - Ben Bangert, 2015-11-14
+
+ - Matthew Desmarais, 2015-11-14
+
+ - Ethan Jucovy, 2015-11-14
+
+ - Marc Abramowitz, 2015-11-14
+
+ - Benjamin Behringer, 2015-11-14
+
+ - Richard Flosi, 2015-11-14
+
+ - Juliusz Gonera, 2015-11-14
+
+ - John Kraal, 2015-11-14
+
+ - Alessandro Molina, 2015-11-14
+
+ - Neil Muller, 2015-11-14
+
+ - Philipp Spitzer, 2015-11-14
+
+ - Vitaly Babiy, 2015-11-14
+
+ - David Stanek, 2015-11-14
+
+ - Ira, 2015-11-14
+
+ - Hong Minhee, 2015-11-14
+
+ - Felix Wolfsteller, 2015-11-14
+
+ - Keith Yang, 2015-11-14
+
+ - Elmer de Looff, 2015-11-14
+
+ - Jonathan Vanasco, 2015-11-14
+
+ - Luke Tucker, 2015-11-14
+
+ - Kevin Dangoor, 2015-11-14
+
+ - Mark Lipscombe, 2015-11-14
+
+ - Nina Plakalovic, 2015-11-14
+
+ - Oleg Broytman, 2015-11-14
+
+ - Christophe Benz, 2015-11-14
+
+ - Chris George, 2015-11-14
+
+ - Philipp Hagemeister, 2015-11-14
+
+ - Remco Verhoef, 2015-11-14
+
+ - Ryan P. Kelly, 2015-11-14
+
+ - Jason R. Coombs, 2015-11-14
+
+ - Joshua Bronson, 2015-11-14
+
+ - Aleksey Sivokon, 2015-11-14
+
+ - Stéphane Raimbault 2015-11-23
+
+ - Jason Culverhouse, 2015-11-24
+
+ - Michele Cella, 2015-11-30
+
+ - Arnaud Fontaine, 2015-12-01
+
+ - Bradley Arsenault, 2015-12-05
+
+ - Bo Du, 2015-12-28
+
+ - Miloš Gavrilov, 2015-12-06
+
+
+The following people made their contributions under the PSF license and did
+not respond to requests to approve the change to the MIT license. If you are
+one of these contributors and you do not want you changes licensed under MIT
+please contact the project managers and we will work on removing your
+contributions.
+
+ - Patrick Hunt
+
+ - Dennis Muhlestein
+
+ - Marius Gedminas
+
+ - Jacob Smullyan
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 00000000..20bd89b8
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2015 Ian Bicking and FormEncode Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
index 8e84270e..549bdeb8 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,10 +1,26 @@
-recursive-include . *.py
-recursive-include docs *.html *.txt *.css
-recursive-include formencode/javascript *.js
-recursive-include formencode/i18n *.py *.pot *.po *.mo
-recursive-include formencode/tests *.txt
-recursive-exclude . *.pyc *.pyo *~
+exclude .gitignore
+exclude .gitattributes
+exclude .readthedocs.yaml
+
+include MANIFEST.in
+
+include LICENSE.txt
+include README.rst
+
+include tox.ini
+
+graft src/formencode
+graft src/formencode/javascript
+include src/formencode/i18n/*.pot
+recursive-include src/formencode/i18n *.po *.mo
+
+graft examples
+recursive-include examples *.py
+
+graft tests
+graft tests/htmlfill_data
+recursive-include docs *.rst conf.py Makefile make.bat
prune docs/_build
-prune **/.svn
-prune .hg
-prune .git
+
+global-exclude */__pycache__/*
+global-exclude *.py[cod]
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..4a5d06db
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+init:
+ pip install -e .
+ pip install -r requirements-test.txt
+
+.PHONY: tests
+
+tests:
+ pytest tests
+
+flake8:
+ flake8 src tests
+
+coverage:
+ pytest --cov-config .coveragerc --verbose --cov-report term --cov-report xml --cov=formencode formencode
+
+.PHONY: docs
+
+docs:
+ cd docs && make html
+ @echo "\033[95m\n\nBuild successful! View the docs homepage at docs/_build/html/index.html.\n\033[0m"
diff --git a/README.rst b/README.rst
index 335b4ef3..76d97684 100644
--- a/README.rst
+++ b/README.rst
@@ -1,30 +1,46 @@
FormEncode
==========
-.. image:: https://secure.travis-ci.org/formencode/formencode.png?branch=master
- :target: https://travis-ci.org/formencode/formencode
- :alt: Travis CI continuous integration status
+|PyPI| |Python| |Tests|
+
+.. |PyPI| image:: https://img.shields.io/pypi/v/formencode
+ :target: https://pypi.org/project/formencode
+ :alt: PyPI
+
+.. |Python| image:: https://img.shields.io/pypi/pyversions/formencode
+ :target: https://pypi.org/project/formencode
+ :alt: PyPI - Python Version
+
+.. |Tests| image:: https://github.com/formencode/formencode/actions/workflows/run-tests.yml/badge.svg
+ :target: https://github.com/formencode/formencode/actions
+ :alt: Test Status
Introduction
------------
-FormEncode is a validation and form generation package. The
-validation can be used separately from the form generation. The
-validation works on compound data structures, with all parts being
-nestable. It is separate from HTTP or any other input mechanism.
+FormEncode is a validation and form generation package.
+The validation can be used separately from the form generation.
+The validation works on compound data structures, with all parts being nestable.
+It is separate from HTTP or any other input mechanism.
Documentation
-------------
-The latest documentation is available at http://www.formencode.org/
+The latest documentation is available at
+`www.formencode.org `_.
-Changes
+
+Testing
-------
-Added a validator that can require one or more fields based on the value of another field.
+Use ``pytest formencode`` to run the test suite.
+Use ``tox`` to run the test suite for all supported Python versions.
+
-A german howto can be found here: http://techblog.auf-nach-mallorca.info/2014/08/19/dynamische_formulare_validieren_mit_formencode/
+Changes
+-------
-Courtesy of the developers of http://www.auf-nach-mallorca.info
+See the `What's New `_
+section of the documentation.
diff --git a/docs/Validator.txt b/docs/Validator.txt
index 91e68deb..7dc671c1 100644
--- a/docs/Validator.txt
+++ b/docs/Validator.txt
@@ -35,9 +35,9 @@ The basic metaphor for validation is **to_python** and
-- the trusted application, your own Python objects. The "other" may
be a web form, an external database, an XML-RPC request, or any data
source that is not completely trusted or does not map directly to
-Python's object model. :meth:`to_python` is the process of taking
-external data and preparing it for internal use, :meth:`from_python`
-generally reverses this process (:meth:`from_python` is usually the less
+Python's object model. ``to_python()`` is the process of taking
+external data and preparing it for internal use, ``from_python()``
+generally reverses this process (``from_python()`` is usually the less
interesting of the pair, but provides some important features).
The core of this validation process is two methods and an exception::
@@ -52,31 +52,29 @@ The core of this validation process is two methods and an exception::
...
Invalid: Please enter an integer value
-``"ten"`` isn't a valid integer, so we get a :class:`formencode.Invalid`
-exception. Typically we'd catch that exception, and use it for some
+``"ten"`` isn't a valid integer, so we get a :class:`~formencode.api.Invalid`
+exception. Typically, we'd catch that exception, and use it for some
sort of feedback. Like:
-.. comment (fake raw_input):
+.. comment (fake input):
- >>> raw_input_input = []
- >>> def raw_input(prompt):
- ... value = raw_input_input.pop(0)
- ... print '%s%s' % (prompt, value)
+ >>> input_values = []
+ >>> def input(prompt):
+ ... value = input_values.pop(0)
+ ... print ('%s%s' % (prompt, value))
... return value
- >>> raw_input_input.extend(['ten', '10'])
- >>> raw_input_input.extend(['bob', 'bob@nowhere.com'])
- >>> if unicode is str: # Python 3
- ... input = raw_input
+ >>> input_values.extend(['ten', '10'])
+ >>> input_values.extend(['bob', 'bob@nowhere.com'])
::
>>> def get_integer():
... while 1:
... try:
- ... value = raw_input('Enter a number: ')
+ ... value = input('Enter a number: ')
... return validator.to_python(value)
- ... except formencode.Invalid, e:
- ... print e
+ ... except formencode.Invalid as e:
+ ... print (e)
...
>>> get_integer()
Enter a number: ten
@@ -89,17 +87,17 @@ We can also generalize this kind of function::
>>> def valid_input(prompt, validator):
... while 1:
... try:
- ... value = raw_input(prompt)
+ ... value = input(prompt)
... return validator.to_python(value)
- ... except formencode.Invalid, e:
- ... print e
+ ... except formencode.Invalid as e:
+ ... print(e)
>>> valid_input('Enter your email: ', validators.Email())
Enter your email: bob
An email address must contain a single @
Enter your email: bob@nowhere.com
'bob@nowhere.com'
-:class:`Invalid` exceptions generally give a good, user-readable error
+``Invalid`` exceptions generally give a good, user-readable error
message about the problem with the input. Using the exception gets
more complicated when you use compound data structures (dictionaries
and lists), which we'll talk about later__.
@@ -150,24 +148,26 @@ they'll be specific to your application::
... return value
.. note::
- The class :class:`formencode.FancyValidator` is the superclass
+ The class `formencode.FancyValidator` is the superclass
for most validators in FormEncode, and it provides a number of useful
features that most validators can use -- for instance, you can pass
``strip=True`` into any of these validators, and they'll strip
whitespace from the incoming value before any other validation.
-This overrides the internal :meth:`_convert_to_python()` method:
-:class:`formencode.FancyValidator` adds a number of extra features, and then
-calls the internal :meth:`_convert_to_python` method, which is the method you'll
-typically write. Contrary to the external method :meth:`to_python`, its
-only concern is the conversion part, not the validation part. If further
-validation is necessary, this can be done in two other internal methods,
-either :meth:`_validate_python()` or :meth:`_validate_other()`. We will give
-an example for that later. When a validator finds an error, it raises an
-exception (:class:`formencode.Invalid`), with the error message and the
-value and "state" objects. We'll talk about state_ later. Here's the
-other custom validator, that checks passwords against words in the
-standard Unix word file::
+This overrides the internal ``_convert_to_python()`` method:
+:class:`formencode.api.FancyValidator` adds a number of extra features,
+and then calls the internal ``_convert_to_python`` method,
+which is the method you'll typically write.
+Contrary to the external method ``to_python()``, its only concern is
+the conversion part, not the validation part.
+If further validation is necessary,
+this can be done in two other internal methods,
+either ``_validate_python()`` or ``_validate_other()``.
+We will give an example for that later. When a validator finds an error,
+it raises an exception (:class:`formencode.api.Invalid`), with the error
+message and the value and "state" objects. We'll talk about state_ later.
+Here's the other custom validator, that checks passwords against words
+in the standard Unix word file::
>>> class SecurePassword(formencode.FancyValidator):
... words_filename = '/usr/share/dict/words'
@@ -194,8 +194,8 @@ And here's a schema::
... chained_validators = [validators.FieldsMatch(
... 'password', 'password_confirm')]
-Like any other validator, a :class:`Registration` instance will have the
-:meth:`to_python` and :meth:`from_python` methods. The input should be a
+Like any other validator, a ``Registration`` instance will have the
+``to_python`` and ``from_python`` methods. The input should be a
dictionary (or a Paste MultiDict), with keys like ``"first_name"``,
``"password"``, etc. The validators you give as attributes will be
applied to each of the values of the dictionary. *All* the values
@@ -203,26 +203,27 @@ will be validated, so if there are multiple invalid fields you will
get information about all of them.
Most validators (anything that subclasses
-:class:`formencode.FancyValidator`) will take a certain standard
-set of constructor keyword arguments. See
-:class:`formencode.api.FancyValidator` for more -- here we use
-``not_empty=True``.
+:class:`formencode.api.FancyValidator`) will take a certain standard
+set of constructor keyword arguments.
+See :class:`formencode.api.FancyValidator` for more
+-- here we use ``not_empty=True``.
-Another notable validator is :class:`formencode.compound.All` -- this
+Another notable validator is :class:`~formencode.compound.All` -- this
is a *compound validator* -- that is, it's a validator that takes
-validators as input. Schemas are one example; in this case :class:`All`
-takes a list of validators and applies each of them in turn.
-:class:`formencode.compound.Any` is its compliment, that uses the
+validators as input. Schemas are one example; in this case
+:class:`~formencode.compound.All` takes a list of validators
+and applies each of them in turn.
+:class:`~formencode.compound.Any` is its compliment, that uses the
first passing validator in its list.
.. _pre_validators:
.. _chained_validators:
-:attr:`chained_validators` are validators that are run on the entire
-dictionary after other validation is done (:attr:`pre_validators` are
-applied before the schema validation). chained_validators will also
+``chained_validators`` are validators that are run on the entire
+dictionary after other validation is done (``pre_validators`` are
+applied before the schema validation). ``chained_validators`` will also
allow for multiple validators to fail and report to the error_dict
-so, for example, if you have an email_confirm and a password_confirm
+so, for example, if you have an ``email_confirm`` and a ``password_confirm``
fields and use FieldsMatch on both of them as follows:
>>> chained_validators = [
@@ -257,16 +258,17 @@ function that you write. For example::
Invalid: state: You must enter a state
-The :func:`validate_state` function (or any validation function) returns
+The ``validate_state()`` function (or any validation function) returns
any errors in the form (or it may raise Invalid directly). It can
-also modify the :obj:`value_dict` dictionary directly. When it returns
+also modify the ``value_dict`` dictionary directly. When it returns
None this indicates that everything is valid. You can use this with a
-:class:`Schema` by putting :class:`ValidateState` in :attr:`pre_validators`
-(all validation will be done before the schema's validation, and if there's
-an error the schema won't be run). Or you can put it in
-:attr:`chained_validators` and it will be run *after* the schema. If the
-schema fails (the values are invalid) then :class:`ValidateState` will not
-be run, unless you set :attr:`validate_partial_form` to True (like
+:class:`formencode.schema.Schema` by putting ``ValidateState`` in
+``pre_validators`` (all validation will be done before the schema's
+validation, and if there's an error the schema won't be run).
+Or you can put it in ``chained_validators`` and it will be run
+*after* the schema. If the schema fails (the values are invalid)
+then ``ValidateState`` will not be run, unless you set
+``validate_partial_form`` to True (like
``ValidateState = SimpleFormValidator(validate_state,
validate_partial_form=True)``. If you validate a partial form you
should be careful that you handle missing keys and other
@@ -285,12 +287,12 @@ is for::
... title = validators.ByteString(not_empty=True)
>>> validator = formencode.ForEach(BookSchema())
-The :obj:`validator` we've created will take a list of dictionaries as
+The ``validator`` we've created will take a list of dictionaries as
input (like ``[{"id": "1", "title": "War & Peace"}, {"id": "2",
-"title": "Brave New World"}, ...]``). It applies the :class:`BookSchema`
-to each entry, and collects any errors and reraises them. Of course,
-when you are validating input from an HTML form you won't get well
-structured data like this (we'll talk about that later__).
+"title": "Brave New World"}, ...]``). It applies the ``BookSchema``
+to each entry, and collects any errors and re-raises them.
+Of course, when you are validating input from an HTML form you won't
+get well-structured data like this (we'll talk about that later__).
.. __: `HTTP/HTML Form Input`_
@@ -298,9 +300,8 @@ Writing Your Own Validator
--------------------------
We gave a brief introduction to creating a validator earlier
-(:class:`UniqueUsername` and :class:`SecurePassword`). We'll discuss
-that a little more. Here's a more complete implementation of
-:class:`SecurePassword`::
+(``UniqueUsername`` and ``SecurePassword``). We'll discuss that a little
+more. Here's a more complete implementation of ``SecurePassword``::
>>> import re
>>> class SecurePassword(validators.FancyValidator):
@@ -324,37 +325,37 @@ that a little more. Here's a more complete implementation of
...
... def _validate_python(self, value, state):
... if len(value) < self.min:
- ... raise Invalid(self.message("too_few", state,
- ... min=self.min),
- ... value, state)
+ ... raise formencode.Invalid(self.message("too_few", state,
+ ... min=self.min),
+ ... value, state)
... non_letters = self.letter_regex.sub('', value)
... if len(non_letters) < self.non_letter:
- ... raise Invalid(self.message("non_letter", state,
- ... non_letter=self.non_letter),
- ... value, state)
+ ... raise formencode.Invalid(self.message("non_letter", state,
+ ... non_letter=self.non_letter),
+ ... value, state)
With all validators, any arguments you pass to the constructor will be
-used to set instance variables. So :class:`SecureValidator(min=5)` will
+used to set instance variables. So ``SecureValidator(min=5)`` will
be a minimum-five-character validator. This makes it easy to also
subclass other validators, giving different default values.
Unlike the previous implementation we use the already mentioned
-:meth:`_validate_python` method, which is another internal method
-:class:`FancyValidator` allows us to override. :meth:`_validate_python`
-doesn't have any return value, it simply raises an exception if it
-needs to. It validates the value *after* it has been converted
-(by :meth:`_convert_to_python`). :meth:`_validate_other` validates before
+``_validate_python`` method, which is another internal method
+:class:`~formencode.api.FancyValidator` allows us to override.
+``_validate_python`` doesn't have any return value, it simply raises an
+exception if it needs to. It validates the value *after* it has been
+converted (by ``_convert_to_python``). ``_validate_other`` validates before
conversion, but that's usually not that useful. The external method
-:meth:`to_python` cares about the extra features such as the
-:attr:`if_empty` parameter, and uses the internal methods to do the
-actual conversion and validation; first it calls :meth:`_validate_other`,
-then :meth:`_convert_to_python` and at last :meth:`_validate_python`.
+``to_python`` cares about the extra features such as the ``if_empty``
+parameter, and uses the internal methods to do the actual conversion
+and validation; first it calls ``_validate_other``, then
+``_convert_to_python`` and at last ``_validate_python``.
The use of ``self.message(...)`` is meant to make the messages easy to
-format for different environments, and replacable (with translations,
+format for different environments, and replaceable (with translations,
or simply with different text). Each message should have an
identifier (``"min"`` and ``"non_letter"`` in this example). The
-keyword arguments to :meth:`message` are used for message substitution.
+keyword arguments to ``message`` are used for message substitution.
See Messages_ for more.
Other Validator Usage
@@ -370,10 +371,9 @@ to set these. These are (effectively) equivalent::
... regex = '^[a-zA-Z]+$'
>>> plain = Plain()
-You can actually use classes most places where you could use an
-instance; :meth:`.to_python()` and :meth:`.from_python()` will create
-instances as necessary, and many other methods are available on both
-the instance and the class level.
+You can actually use classes most places where you could use an instance;
+``to_python()`` and ``from_python()`` will create instances as necessary,
+and many other methods are available on both the instance and the class level.
When dealing with nested validators this class syntax is often easier
to work with, and better displays the structure.
@@ -381,41 +381,39 @@ to work with, and better displays the structure.
.. _FancyValidator:
There are several options that most validators support (including your
-own validators, if you subclass from :class:`formencode.FancyValidator`):
-
-:attr:`if_empty`:
- If set, then this value will be returned if the input evaluates
- to false (empty list, empty string, None, etc), but not the 0 or
- False objects. This only applies to ``.to_python()``.
-
-:attr:`not_empty`:
- If true, then if an empty value is given raise an error.
- (Both with ``.to_python()`` and also ``.from_python()``
- if ``.validate_python`` is true).
-
-:attr:`strip`:
- If true and the input is a string, strip it (occurs before empty
- tests).
-
-:attr:`if_invalid`:
- If set, then when this validator would raise Invalid during
- ``.to_python()``, instead return this value.
-
-:attr:`if_invalid_python`:
- If set, when the Python value (converted with
- ``.from_python()``) is invalid, this value will be returned.
-
-:attr:`accept_python`:
- If True (the default), then ``._validate_python()`` and
- ``._validate_other()`` will not be called when
- ``.from_python()`` is used.
-
-:attr:`if_missing`:
- Typically when a field is missing the schema will raise an
- error. In that case no validation is run -- so things like
- ``if_invalid`` won't be triggered. This special attribute (if
- set) will be used when the field is missing, and no error will
- occur. (``None`` or ``()`` are common values)
+own validators, if you subclass from :class:`formencode.api.FancyValidator`):
+
+``if_empty``:
+ If set, then this value will be returned if the input evaluates
+ to false (empty list, empty string, None, etc), but not the 0 or
+ False objects. This only applies to ``.to_python()``.
+
+``not_empty``:
+ If true, then if an empty value is given raise an error.
+ (Both with ``.to_python()`` and also ``.from_python()``
+ if ``.validate_python`` is true).
+
+``strip``:
+ If true and the input is a string, strip it (occurs before empty tests).
+
+``if_invalid``:
+ If set, then when this validator would raise Invalid during
+ ``.to_python()``, instead return this value.
+
+``if_invalid_python``:
+ If set, when the Python value (converted with
+ ``.from_python()``) is invalid, this value will be returned.
+
+``accept_python``:
+ If True (the default), then ``._validate_python()`` and
+ ``._validate_other()`` will not be called when ``.from_python()`` is used.
+
+``if_missing``:
+ Typically when a field is missing the schema will raise an
+ error. In that case no validation is run -- so things like
+ ``if_invalid`` won't be triggered. This special attribute (if
+ set) will be used when the field is missing, and no error will
+ occur. (``None`` or ``()`` are common values)
State
-----
@@ -433,47 +431,49 @@ validator know the locale? State! Whatever else you need to pass in,
just put it in the state object as an attribute, then look for that
attribute in your validator.
-Also, during compound validation (a :class:`formencode.schema.Schema`
-or :class:`formencode.foreach.ForEach`) the state (if not None) will
-have more instance variables added to it. During a :class:`Schema`
-(dictionary) validation the instance variable ``key`` and
-``full_dict`` will be added -- ``key`` is the current key (i.e.,
-validator name), and ``full_dict`` is the rest of the values being
-validated. During a :class:`ForEeach` (list) validation, ``index`` and
-``full_list`` will be set.
+Also, during compound validation (a :class:`~formencode.schema.Schema`
+or :class:`~formencode.foreach.ForEach`) the state (if not None) will
+have more instance variables added to it.
+During a :class:`~formencode.schema.Schema` (dictionary) validation
+the instance variable ``key`` and ``full_dict`` will be added -- ``key``
+is the current key (i.e., validator name), and ``full_dict`` is the rest
+of the values being validated.
+During a :class:`~formencode.foreach.ForEach` (list) validation,
+``index`` and ``full_list`` will be set.
Invalid Exceptions
------------------
-Besides the string error message, :class:`formencode.Invalid`
+Besides the string error message, :class:`~formencode.api.Invalid`
exceptions have a few other instance variables:
-:attr:`value`:
+``value``:
The input to the validator that failed.
-:attr:`state`:
+``state``:
The associated state_.
-:attr:`msg`:
+``msg``:
The error message (``str(exc)`` returns this)
-:attr:`error_list`:
- If the exception happened in a ``ForEach`` (list) validator, then
- this will contain a list of ``Invalid`` exceptions. Each item
- from the list will have an entry, either None for no error, or an
- exception.
+``error_list``:
+ If the exception happened in a :class:`~formencode.foreach.ForEach`
+ (list) validator, then this will contain a list of
+ :class:`~formencode.api.Invalid` exceptions. Each item from the list
+ will have an entry, either None for no error, or an exception.
-:attr:`error_dict`:
- If the exception happened in a :class:`Schema` (dictionary) validator,
- then this will contain :class:`Invalid` exceptions for each failing
- field. Passing fields not be included in this dictionary.
+``error_dict``:
+ If the exception happened in a :class:`~formencode.schema.Schema`
+ (dictionary) validator, then this will contain
+ :class:`~formencode.api.Invalid` exceptions for each failing field.
+ Passing fields not be included in this dictionary.
-:meth:`.unpack_errors()`:
+``.unpack_errors()``:
This method returns a set of lists and dictionaries containing
- strings, for each error. It's an unpacking of :attr:`error_list`,
- :attr:`error_dict` and :attr:`msg`. If you get an Invalid exception
- from a :class:`Schema`, you probably want to call this method on the
- exception object.
+ strings, for each error. It's an unpacking of ``error_list``,
+ ``error_dict`` and ``msg``. If you get an Invalid exception
+ from a :class:`~formencode.schema.Schema`, you probably want to call
+ this method on the exception object.
.. _Messages:
@@ -494,15 +494,15 @@ for each substitution. This way you can reorder or even ignore
placeholders in your new message.
When you are creating a validator, for maximum flexibility you should
-use the :meth:`message` method, like::
+use the ``message`` method, like::
messages = {
'key': 'my message (with a %(substitution)s)',
}
def _validate_python(self, value, state):
- raise Invalid(self.message('key', state, substitution='apples'),
- value, state)
+ raise formencode.Invalid(self.message('key', state, substitution='apples'),
+ value, state)
Localization of Error Messages (i18n)
-------------------------------------
@@ -510,20 +510,20 @@ Localization of Error Messages (i18n)
When a failed validation occurs FormEncode tries to output the error
message in the appropriate language. For this it uses the standard
`gettext `_ mechanism of
-python. To translate the message in the appropirate message FormEncode
+python. To translate the message in the appropriate message FormEncode
has to find a gettext function that translates the string. The
language to be translated into and the used domain is determined by
the found gettext function. To serve a standard translation mechanism
and to enable custom translations it looks in the following order to
find a gettext (``_``) function:
-1. method of the :obj:`state` object
+1. method of the ``state`` object
-2. function :func:`__builtin__._`. This function is only used when::
+2. function ``builtin._``. This function is only used when::
- Validator.use_builtin_gettext == True #True is default
+ Validator.use_builtin_gettext == True # True is default
-3. formencode builtin :func:`_stdtrans` function
+3. formencode builtin ``_stdtrans`` function
for standalone use of FormEncode. The language to use is determined
out of the locale system (see gettext documentation). Optionally you
@@ -536,7 +536,7 @@ find a gettext (``_``) function:
messages in the directory
``localedir/language/LC_MESSAGES/FormEncode.mo``
-4. Custom gettext function and addtional parameters
+4. Custom gettext function and additional parameters
If you use a custom gettext function and you want FormEncode to
call your function with additional parameters you can set the
@@ -572,12 +572,12 @@ Optionally you can also add a test of your language to
ne = formencode.validators.NotEmpty()
[...]
def test_de():
- _test_lang("de", u"Bitte einen Wert eingeben")
+ _test_lang("de", "Bitte einen Wert eingeben")
And the test for your language::
def test_():
- _test_lang("", u"")
+ _test_lang("", "")
HTTP/HTML Form Input
--------------------
diff --git a/docs/_static/formencode.png b/docs/_static/formencode.png
new file mode 100644
index 00000000..6595c523
Binary files /dev/null and b/docs/_static/formencode.png differ
diff --git a/docs/conf.py b/docs/conf.py
index 4bb92a93..ac4a06dd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
# FormEncode documentation build configuration file, created by
# sphinx-quickstart on Tue Aug 30 2011.
#
@@ -11,12 +9,10 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import sys, os
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.insert(0, os.path.abspath('..'))
+# import os
+# import sys
+# sys.path.insert(0, os.path.join(os.path.dirname(
+# os.path.dirname(os.path.abspath(__file__))), 'src'))
# -- General configuration -----------------------------------------------------
@@ -33,28 +29,23 @@
# The suffix of source filenames.
source_suffix = '.txt'
-# The encoding of source files.
-#source_encoding = 'utf-8-sig'
-
# The master toctree document.
master_doc = 'index'
# General information about the project.
-project = u'FormEncode'
-copyright = u'2008-2012, Ian Bicking and Contributors'
+project = 'FormEncode'
+copyright = '2008-2023, Ian Bicking and Contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
-#
-# The short X.Y version.
-version = '1.3'
-# The full version, including alpha/beta/rc tags.
-release = '1.3.0dev'
+from pkg_resources import get_distribution
+release = get_distribution('formencode').version
+version = '.'.join(release.split('.')[:2])
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
-#language = None
+language = 'en'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -179,8 +170,8 @@
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
- ('index', 'FormEncode.tex', u'FormEncode Documentation',
- u'Ian Bicking', 'manual'),
+ ('index', 'FormEncode.tex', 'FormEncode Documentation',
+ 'Ian Bicking', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -212,6 +203,6 @@
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
- ('index', 'formencode', u'FormEncode Documentation',
- [u'Ian Bicking'], 1)
+ ('index', 'formencode', 'FormEncode Documentation',
+ ['Ian Bicking'], 1)
]
diff --git a/docs/index.txt b/docs/index.txt
index 9c9f8a6a..c9efb354 100644
--- a/docs/index.txt
+++ b/docs/index.txt
@@ -21,6 +21,7 @@ What's New
.. toctree::
:maxdepth: 1
+ whatsnew-2.0
whatsnew-1.3
whatsnew-1.2.5
whatsnew-0-to-1.2.4
diff --git a/docs/modules/htmlfill.txt b/docs/modules/htmlfill.txt
index ad13455c..0cff1dd1 100644
--- a/docs/modules/htmlfill.txt
+++ b/docs/modules/htmlfill.txt
@@ -13,5 +13,4 @@ Module Contents
.. autofunction:: escape_formatter
.. autofunction:: escapenl_formatter
.. autofunction:: ignore_formatter
-.. autofunction:: ignore_formatter
.. autoclass:: FillingParser
diff --git a/docs/modules/schema.txt b/docs/modules/schema.txt
index f877abcb..c78d9856 100644
--- a/docs/modules/schema.txt
+++ b/docs/modules/schema.txt
@@ -8,3 +8,5 @@ Module Contents
.. autoclass:: Schema
+.. autoclass:: SimpleFormValidator
+
diff --git a/docs/whatsnew-0-to-1.2.4.txt b/docs/whatsnew-0-to-1.2.4.txt
index b5e5ca47..94a4d6d9 100644
--- a/docs/whatsnew-0-to-1.2.4.txt
+++ b/docs/whatsnew-0-to-1.2.4.txt
@@ -60,7 +60,7 @@ What's New In FormEncode 0 to 1.2.4
* Added :class:`formencode.validators.IPAddress`, validating IP
addresses, from Leandro Lucarella.
-* Added :meth:`formencode.api.Invalid.__unicode__`
+* Added method ``Invalid.__unicode__``
* In :mod:`formencode.htmlfill` use a default encoding of utf8 when
handling mixed ``str``/``unicode`` content. Also do not modify
@@ -125,7 +125,7 @@ What's New In FormEncode 0 to 1.2.4
```` -- this will
cause the error to be swallowed, not shown to the user.
-* Added :class:`formencode.validators.XRI` for validation i-names,
+* Added ``formencode.validators.XRI`` for validation i-names,
i-numbers, URLs, etc (as used in OpenID).
* Look in ``/usr/share/locale`` for locale files, in addition to the
diff --git a/docs/whatsnew-1.2.5.txt b/docs/whatsnew-1.2.5.txt
index c4ea4d30..9b4a8104 100644
--- a/docs/whatsnew-1.2.5.txt
+++ b/docs/whatsnew-1.2.5.txt
@@ -33,5 +33,5 @@ Backwards Incompatibilities
Documentation Enhancements
--------------------------
-- Superceded news with whatsnew documents that will be archived for each
+- Superseded news with whatsnew documents that will be archived for each
version. Archived all news prior to 1.2.5 in :doc:`/whatsnew-0-to-1.2.4`.
diff --git a/docs/whatsnew-1.3.txt b/docs/whatsnew-1.3.txt
index e194b3d2..bea1951c 100644
--- a/docs/whatsnew-1.3.txt
+++ b/docs/whatsnew-1.3.txt
@@ -27,6 +27,4 @@ Backwards Incompatibilities
- The `FancyValidator` methods `_to_python`, `_from_python`, `validate_python` and `validate_other` have been renamed to `_convert_to_python`, `_convert_from_python`, `_validate_python` and `_validate_other`, respectively. This has been done to clarify that while these methods are meant to be overridden by custom validators, they are not part of the external API. They are only helper methods that are called internally by the external methods `to_python` and `from_python`, which constitute the external API. Particularly, do not assume that `_validate_python` catches all validation errors that a call of `to_python` will catch. Please have a look at the `FancyValidator` docstring and source if you're unsure how these methods work together. For the same reason, the `CompoundValidator` method `attempt_convert` has been renamed to `_attempt_convert`. For now, the old method names will still work, but they will output deprecation warnings.
-Documentation Enhancements
---------------------------
-
+ - (1.3.1) Don't use universal wheel
diff --git a/docs/whatsnew-2.0.txt b/docs/whatsnew-2.0.txt
new file mode 100644
index 00000000..9a54e515
--- /dev/null
+++ b/docs/whatsnew-2.0.txt
@@ -0,0 +1,57 @@
+What's New In FormEncode 2.x
+============================
+
+This article explains the latest changes in `FormEncode` version 2.x
+compared to its predecessor, `FormEncode` version 1.3.
+
+2.1.1
+-----
+
+ - Add support for 3.13
+ - Don't require `legacy-cgi` to be installed on 3.13 and later
+ (`#176 `_)
+ - Don't permit `FieldStorageUploadConverter` to be instantiated without
+ having `legacy-cgi` installed since it does not make sense
+ - Releases are now automated through GitHub Actions
+ (`#184 `_)
+
+
+Note: This is the last version that will support Python 3.7 and 3.8 as
+those are now out of support.
+
+2.1.0
+-----
+
+ - Add support for Python 3.7 to 3.12, end support for older Python versions
+ - Context.set() now works as a context manager
+ - Fix binary of swedish translation
+ - Some internal code cleanup and modernization
+
+2.0.1
+-----
+
+ - Add support for Python 3.10
+ - use Pytest instead of Nose and GitHub Actions instead of Travis for tests
+ - Documentation updates
+ - Note this will be the last version to support Python 2.
+ The next version will be 2.1.0 to signal this change.
+ For compatibility with older Python versions, please use versions < 2.1.
+
+2.0.0
+-----
+
+ - `FormEncode` can now run on Python 3.6 and higher without needing to run 2to3 first.
+ - `FormEncode` 2.0 is no longer compatible with Python 2.6 and 3.2 to 3.5.
+ For compatibility with older Python versions, please use versions < 1.3.
+ - This will be the last major version to support Python 2.
+ - Add strict flag to ``USPostalCode`` to raise error on postal codes that has too
+ many digits instead of just truncating
+ - Various Python 3 fixes
+ - Serbian latin translation
+ - Changed License to MIT
+ - Dutch, UK, Greek and South Korean postal code format fixes
+ - Add postal code formats for Switzerland, Cyprus, Faroe Islands, San Marino, Ukraine and Vatican City.
+ - Add ISODateTimeConverter validator
+ - Add ability to target ``htmlfill`` to particular form or ignore a form
+ - Fix format errors in some translations
+ - The version of the library can be checked using ``formencode.__version__``
diff --git a/examples/WebwareExamples/index.py b/examples/WebwareExamples/index.py
index b261e6b5..4a3fdd7b 100644
--- a/examples/WebwareExamples/index.py
+++ b/examples/WebwareExamples/index.py
@@ -72,10 +72,10 @@ def save(self):
fields = self.request().fields()
try:
fields = FormSchema.to_python(fields, self)
- except Invalid, e:
- errors = dict((k, v.encode('utf-8'))
- for k, v in e.unpack_errors().iteritems())
- print "Errors:", errors
+ except Invalid as e:
+ errors = {k: v.encode('utf-8')
+ for k, v in e.unpack_errors().items()}
+ print("Errors:", errors)
self.rendered_form = htmlfill.render(form_template,
defaults=fields, errors=errors)
self.writeHTML()
@@ -83,7 +83,7 @@ def save(self):
self.doAction(fields)
def doAction(self, fields):
- print "Fields:", fields
+ print("Fields:", fields)
self.rendered_form = response_template % fields
self.writeHTML()
diff --git a/formencode/__init__.py b/formencode/__init__.py
deleted file mode 100644
index de1de205..00000000
--- a/formencode/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# formencode package
-
-from formencode.api import (
- NoDefault, Invalid, Validator, Identity,
- FancyValidator, is_empty, is_validator)
-from formencode.schema import Schema
-from formencode.compound import CompoundValidator, Any, All, Pipe
-from formencode.foreach import ForEach
-from formencode import validators
-from formencode import national
-from formencode.variabledecode import NestedVariables
diff --git a/formencode/i18n/sr/LC_MESSAGES/FormEncode.mo b/formencode/i18n/sr/LC_MESSAGES/FormEncode.mo
deleted file mode 100644
index 1c4d146e..00000000
Binary files a/formencode/i18n/sr/LC_MESSAGES/FormEncode.mo and /dev/null differ
diff --git a/formencode/i18n/sv/LC_MESSAGES/FormEncode.mo b/formencode/i18n/sv/LC_MESSAGES/FormEncode.mo
deleted file mode 100644
index aa132e37..00000000
Binary files a/formencode/i18n/sv/LC_MESSAGES/FormEncode.mo and /dev/null differ
diff --git a/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.mo b/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.mo
deleted file mode 100644
index da78e02f..00000000
Binary files a/formencode/i18n/zh_TW/LC_MESSAGES/FormEncode.mo and /dev/null differ
diff --git a/formencode/tests/__init__.py b/formencode/tests/__init__.py
deleted file mode 100644
index a380a390..00000000
--- a/formencode/tests/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import sys
-import os
-
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
-
-# Make sure messages are not translated when running the tests
-# (setting the environment variable here may be too late already,
-# in this case you must set it manually before running the tests).
-os.environ['LANGUAGE'] = 'C'
-
-# Enable deprecation warnings (disabled by default in Python > 2.6).
-import warnings
-warnings.simplefilter('default')
-
-import pkg_resources
-pkg_resources.require('FormEncode')
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..8a0985f0
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,5 @@
+[build-system]
+requires = ["setuptools", "wheel", "setuptools_scm>=7"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 00000000..787b78f4
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,3 @@
+# content of pytest.ini
+[pytest]
+#addopts = --doctest-modules
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 00000000..7a52c582
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,5 @@
+build
+wheel
+flake8
+sphinx
+-r requirements-test.txt
diff --git a/requirements-docs.txt b/requirements-docs.txt
new file mode 100644
index 00000000..ac1ea171
--- /dev/null
+++ b/requirements-docs.txt
@@ -0,0 +1,2 @@
+sphinx >=7, <8
+-e .
diff --git a/requirements-test.txt b/requirements-test.txt
new file mode 100644
index 00000000..4582cadb
--- /dev/null
+++ b/requirements-test.txt
@@ -0,0 +1,5 @@
+dnspython >= 2
+pycountry
+pytest
+pytest-cov
+wheel
diff --git a/run-tests-generate-coverage-html b/run-tests-generate-coverage-html
deleted file mode 100755
index a73069f1..00000000
--- a/run-tests-generate-coverage-html
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-nosetests --with-coverage --cover-package=formencode --cover-erase --cover-html --cover-html-dir=coverage
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 25332a75..7ebd9621 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,51 @@
-[nosetests]
-detailed-errors = 1
+[metadata]
+name = FormEncode
+description = "HTML form validation, generation, and conversion package"
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+author = Ian Bicking
+author_email = ianb@colorstudy.com
+maintainer = Chris Lambacher
+maintainer_email = chris@kateandchris.net
+url = https://www.formencode.org/
+license = MIT
+license_files = LICENSE.txt
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Intended Audience :: Developers
+ License :: OSI Approved :: MIT License
+ Programming Language :: Python
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3 :: Only
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
+ Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3.12
+ Programming Language :: Python :: 3.13
+ Programming Language :: Python :: Implementation :: CPython
+ Programming Language :: Python :: Implementation :: PyPy
+ Topic :: Software Development :: Libraries :: Python Modules
+
+[options]
+package_dir =
+ = src
+packages =
+ formencode
+python_requires = >=3.7
+include_package_data = False
+
+[options.package_data]
+formencode =
+ javascript/*.js
+ i18n/**/*.mo
+
+[options.extras_require]
+testing =
+ pytest
+ dnspython >= 2
+ pycountry
# Babel configuration
[compile_catalog]
@@ -25,6 +71,9 @@ input_file = formencode/i18n/FormEncode.pot
output_dir = formencode/i18n
previous = true
-[wheel]
-universal = 1
+[pep8]
+max_line_length=88
+[flake8]
+max-line-length = 88
+extend-ignore = E128,E402,E501,E731
diff --git a/setup.py b/setup.py
index fdd032cc..0b62acc5 100755
--- a/setup.py
+++ b/setup.py
@@ -5,46 +5,6 @@
The official repo is at GitHub: https://github.com/formencode/formencode
"""
+from setuptools import setup
-import sys
-from setuptools import setup, find_packages
-
-version = '1.3.0a1'
-
-if not '2.6' <= sys.version < '3.0' and not '3.2' <= sys.version:
- raise ImportError('Python version not supported')
-
-tests_require = ['nose', 'pycountry',
- 'dnspython' if sys.version < '3.0' else 'dnspython3']
-
-doctests = ['docs/htmlfill.txt', 'docs/Validator.txt',
- 'formencode/tests/non_empty.txt']
-
-setup(name='FormEncode',
- version=version,
- # requires_python='>=2.6,!=3.0,!=3.1', # PEP345
- description="HTML form validation, generation, and conversion package",
- long_description=__doc__,
- classifiers=[
- "Development Status :: 4 - Beta",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: Python Software Foundation License",
- "Programming Language :: Python",
- "Programming Language :: Python :: 2",
- "Programming Language :: Python :: 3",
- "Topic :: Software Development :: Libraries :: Python Modules",
- ],
- author='Ian Bicking',
- author_email='ianb@colorstudy.com',
- url='http://formencode.org',
- license='PSF',
- zip_safe=False,
- packages=find_packages(),
- include_package_data=True,
- package_data={'formencode': ['../docs/*.txt']},
- test_suite='formencode.tests',
- tests_require=tests_require,
- extras_require={'testing': tests_require},
- use_2to3=True,
- convert_2to3_doctests=doctests,
- )
+setup()
diff --git a/src/formencode/__init__.py b/src/formencode/__init__.py
new file mode 100644
index 00000000..fde4099a
--- /dev/null
+++ b/src/formencode/__init__.py
@@ -0,0 +1,32 @@
+"""The formencode package"""
+
+try:
+ from importlib.metadata import version, PackageNotFoundError
+except ImportError: # Python < 3.8
+ from pkg_resources import get_distribution
+ from pkg_resources import DistributionNotFound as PackageNotFoundError
+
+ def version(distribution_name):
+ return get_distribution(distribution_name).version
+
+from formencode.api import (
+ NoDefault, Invalid, Validator, Identity,
+ FancyValidator, is_empty, is_validator)
+from formencode.schema import Schema
+from formencode.compound import CompoundValidator, Any, All, Pipe
+from formencode.foreach import ForEach
+from formencode import validators
+from formencode import national
+from formencode.variabledecode import NestedVariables
+
+__all__ = [
+ 'NoDefault', 'Invalid', 'Validator', 'Identity', 'FancyValidator',
+ 'is_empty', 'is_validator',
+ 'Schema', 'CompoundValidator', 'Any', 'All', 'Pipe',
+ 'ForEach', 'validators', 'national', 'NestedVariables',
+ '__version__']
+
+try:
+ __version__ = version(__name__)
+except PackageNotFoundError: # package is not installed
+ __version__ = 'local-test'
diff --git a/formencode/api.py b/src/formencode/api.py
similarity index 85%
rename from formencode/api.py
rename to src/formencode/api.py
index 4f7655cd..88c62b39 100644
--- a/formencode/api.py
+++ b/src/formencode/api.py
@@ -2,52 +2,43 @@
Core classes for validation.
"""
-from . import declarative
import gettext
import os
import re
import textwrap
import warnings
-try:
- from pkg_resources import resource_filename
-except ImportError:
- resource_filename = None
+from . import declarative
__all__ = ['NoDefault', 'Invalid', 'Validator', 'Identity',
'FancyValidator', 'is_empty', 'is_validator']
def get_localedir():
- """Retrieve the location of locales.
-
- If we're built as an egg, we need to find the resource within the egg.
- Otherwise, we need to look for the locales on the filesystem or in the
- system message catalog.
+ """Retrieve the location of locales."""
+ file_dir = os.path.join(os.path.dirname(__file__), 'i18n')
+ if not hasattr(os, 'access'):
+ # this happens on Google App Engine
+ return file_dir
- """
- locale_dir = ''
- # Check the egg first
- if resource_filename is not None:
- try:
- locale_dir = resource_filename(__name__, "/i18n")
- except NotImplementedError:
- # resource_filename doesn't work with non-egg zip files
+ import importlib.resources as resources
+ # try to get it via the resource
+ pkg_name = __name__.split('.', 1)[0]
+ try:
+ resource_dir = resources.files(pkg_name) / 'i18n'
+ except AttributeError: # Python < 3.11
+ with resources.path(pkg_name, 'i18n') as resource_dir:
pass
- if not hasattr(os, 'access'):
- # This happens on Google App Engine
- return os.path.join(os.path.dirname(__file__), 'i18n')
- if os.access(locale_dir, os.R_OK | os.X_OK):
- # If the resource is present in the egg, use it
- return locale_dir
+ if os.access(resource_dir, os.R_OK | os.X_OK):
+ # if the resource is present, use it
+ return resource_dir
- # Otherwise, search the filesystem
- locale_dir = os.path.join(os.path.dirname(__file__), 'i18n')
- if not os.access(locale_dir, os.R_OK | os.X_OK):
- # Fallback on the system catalog
- locale_dir = os.path.normpath('/usr/share/locale')
+ # otherwise, search the filesystem
+ if os.access(file_dir, os.R_OK | os.X_OK):
+ return file_dir
- return locale_dir
+ # fallback on the system catalog
+ return os.path.normpath('/usr/share/locale')
def set_stdtranslation(domain="FormEncode", languages=None,
@@ -57,18 +48,17 @@ def set_stdtranslation(domain="FormEncode", languages=None,
languages=languages,
localedir=localedir, fallback=True)
global _stdtrans
- try:
- _stdtrans = t.ugettext
- except AttributeError: # Python 3
- _stdtrans = t.gettext
+ _stdtrans = t.gettext
+
set_stdtranslation()
# Dummy i18n translation function, nothing is translated here.
-# Instead this is actually done in api.Validator.message.
+# Instead, this is actually done in api.Validator.message.
# The surrounding _('string') of the strings is only for extracting
# the strings automatically.
-# If you run pygettext with this source comment this function out temporarily.
+# If you run pygettext with this source, comment this function out temporarily.
+
_ = lambda s: s
@@ -90,13 +80,13 @@ def inner(*args, **kwargs):
return outer
-class NoDefault(object):
+class NoDefault:
"""A dummy value used for parameters with no default."""
def is_empty(value):
"""Check whether the given value should be considered "empty"."""
- return value is None or value == '' or (
+ return value is None or value == '' or value == b'' or (
isinstance(value, (list, tuple, dict)) and not value)
@@ -147,18 +137,7 @@ def __init__(self, msg,
% (self, self.error_list, self.error_dict))
def __str__(self):
- val = self.msg
- return val
-
- if unicode is not str: # Python 2
-
- def __unicode__(self):
- if isinstance(self.msg, unicode):
- return self.msg
- elif isinstance(self.msg, str):
- return self.msg.decode('utf8')
- else:
- return unicode(self.msg)
+ return self.msg
def unpack_errors(self, encode_variables=False, dict_char='.',
list_char='-'):
@@ -177,15 +156,15 @@ def unpack_errors(self, encode_variables=False, dict_char='.',
for item in self.error_list]
if self.error_dict:
result = {}
- for name, item in self.error_dict.iteritems():
+ for name, item in self.error_dict.items():
result[name] = item if isinstance(
- item, basestring) else item.unpack_errors()
+ item, str) else item.unpack_errors()
if encode_variables:
from . import variabledecode
result = variabledecode.variable_encode(
result, add_repetitions=False,
dict_char=dict_char, list_char=list_char)
- for key in result.keys():
+ for key in list(result.keys()):
if not result[key]:
del result[key]
return result
@@ -194,9 +173,7 @@ def unpack_errors(self, encode_variables=False, dict_char='.',
return self.msg
-############################################################
-## Base Classes
-############################################################
+# Base Classes
class Validator(declarative.Declarative):
@@ -245,8 +222,8 @@ def message(self, msgName, state, **kw):
except AttributeError:
try:
if self.use_builtins_gettext:
- import __builtin__
- trans = __builtin__._
+ import builtins
+ trans = builtins._
else:
trans = _stdtrans
except AttributeError:
@@ -255,8 +232,7 @@ def message(self, msgName, state, **kw):
if not callable(trans):
trans = _stdtrans
- msg = self._messages[msgName]
- msg = trans(msg, **self.gettextargs)
+ msg = trans(self._messages[msgName], **self.gettextargs)
try:
return msg % kw
@@ -309,11 +285,10 @@ def _initialize_docstring(cls):
This changes the class's docstring to include information
about all the messages this validator uses.
"""
- doc = cls.__doc__ or ''
- doc = [textwrap.dedent(doc).rstrip()]
- messages = sorted(cls._messages.iteritems())
- doc.append('\n\n**Messages**\n\n')
- for name, default in messages:
+ doc = [
+ textwrap.dedent(cls.__doc__ or '').rstrip(),
+ '\n\n**Messages**\n\n']
+ for name, default in sorted(cls._messages.items()):
default = re.sub(r'(%\(.*?\)[rsifcx])', r'``\1``', default)
doc.append('``' + name + '``:\n')
doc.append(' ' + default + '\n\n')
@@ -325,6 +300,7 @@ class _Identity(Validator):
def __repr__(self):
return 'validators.Identity'
+
Identity = _Identity()
@@ -451,12 +427,11 @@ def __classinit__(cls, new_attrs):
stacklevel=cls._inheritance_level + 2)
setattr(cls, new, new_attrs[old])
elif new in new_attrs:
- setattr(cls, old, deprecated(old=old, new=new)(
- new_attrs[new]))
+ setattr(cls, old, deprecated(old=old, new=new)(new_attrs[new]))
def to_python(self, value, state=None):
try:
- if self.strip and isinstance(value, basestring):
+ if self.strip and isinstance(value, str):
value = value.strip()
elif hasattr(value, 'mixed'):
# Support Paste's MultiDict
@@ -484,7 +459,7 @@ def to_python(self, value, state=None):
def from_python(self, value, state=None):
try:
- if self.strip and isinstance(value, basestring):
+ if self.strip and isinstance(value, str):
value = value.strip()
if not self.accept_python:
if self.is_empty(value):
@@ -520,7 +495,7 @@ def empty_value(self, value):
return None
def assert_string(self, value, state):
- if not isinstance(value, basestring):
+ if not isinstance(value, str):
raise Invalid(self.message('badType', state,
type=type(value), value=value),
value, state)
diff --git a/formencode/compound.py b/src/formencode/compound.py
similarity index 89%
rename from formencode/compound.py
rename to src/formencode/compound.py
index e01c4e00..5caf950d 100644
--- a/formencode/compound.py
+++ b/src/formencode/compound.py
@@ -1,16 +1,13 @@
"""
Validators for applying validations in sequence.
"""
-
-from .api import (FancyValidator, Identity, Invalid, NoDefault, Validator,
- is_validator)
+from .api import (
+ FancyValidator, Identity, Invalid, NoDefault, Validator, is_validator)
__all__ = ['CompoundValidator', 'Any', 'All', 'Pipe']
-############################################################
-## Compound Validators
-############################################################
+# Compound Validators
def to_python(validator, value, state):
return validator.to_python(value, state)
@@ -38,14 +35,14 @@ class CompoundValidator(FancyValidator):
@staticmethod
def __classinit__(cls, new_attrs):
FancyValidator.__classinit__(cls, new_attrs)
- toAdd = []
- for name, value in new_attrs.iteritems():
+ to_add = []
+ for name, value in new_attrs.items():
if is_validator(value) and value is not Identity:
- toAdd.append((name, value))
+ to_add.append((name, value))
# @@: Should we really delete too?
delattr(cls, name)
- toAdd.sort()
- cls.validators.extend([value for _name, value in toAdd])
+ to_add.sort()
+ cls.validators.extend([value for _name, value in to_add])
def __init__(self, *args, **kw):
Validator.__init__(self, *args, **kw)
@@ -133,12 +130,12 @@ def accept_iterator(self):
class All(CompoundValidator):
- """Check if all of the specified validators are valid.
+ """Check that specified validators are valid.
- This class is like an 'and' operator for validators. All
- validators must work, and the results are passed in turn through
- all validators for conversion in the order of evaluation. All
- is the same as `Pipe` but operates in the reverse order.
+ This class is like an 'and' operator for validators.
+ All validators must work, and the results are passed in turn through
+ all validators for conversion in the order of evaluation.
+ ``All`` is the same as ``Pipe`` but operates in the reverse order.
The order of evaluation differs depending on if you are validating to
Python or from Python as follows:
@@ -147,7 +144,7 @@ class All(CompoundValidator):
The validators are evaluated left to right when validating from Python.
- `Pipe` is more intuitive when predominantly validating to Python.
+ ``Pipe`` is more intuitive when predominantly validating to Python.
Examples::
@@ -200,7 +197,7 @@ def join(cls, *validators):
filtering out None and trying to keep `All` validators from
being nested (which isn't needed).
"""
- validators = filter(lambda v: v and v is not Identity, validators)
+ validators = [v for v in validators if v and v is not Identity]
if not validators:
return Identity
if len(validators) == 1:
diff --git a/formencode/context.py b/src/formencode/context.py
similarity index 90%
rename from formencode/context.py
rename to src/formencode/context.py
index 8bd8c72a..f1c0b68b 100644
--- a/formencode/context.py
+++ b/src/formencode/context.py
@@ -25,7 +25,7 @@ def do_stuff():
context object to pass information to another thread. In a
single-thread environment it doesn't really matter.
-Typically you will create ``Context`` instances for your application,
+Typically, you will create ``Context`` instances for your application,
environment, etc. These should be global module-level variables, that
may be imported by any interested module; each instance is a namespace
of its own.
@@ -41,12 +41,12 @@ def do_stuff():
value. ``None`` is a typical value for that.
Another is ``context.set_default(**vars)``, which will set only those
-variables to default values. This will not effect the stack of
-scopes, but will only add defaults.
+variables to default values. This will not affect the stack of scopes,
+but will only add defaults.
-When Python 2.5 comes out, this syntax would certainly be useful::
+A context can be also used like this::
- with context(page='view'):
+ with context.set(page='view'):
do stuff...
And ``page`` will be set to ``'view'`` only inside that ``with`` block.
@@ -61,7 +61,7 @@ def do_stuff():
_restore_ids = count()
-class NoDefault(object):
+class NoDefault:
"""A dummy value used for parameters with no default."""
@@ -69,7 +69,7 @@ class ContextRestoreError(Exception):
"""Raised when something is restored out-of-order."""
-class Context(object):
+class Context:
def __init__(self, default=NoDefault):
self.__dict__['_local'] = threading.local()
@@ -93,10 +93,10 @@ def __getattr__(self, attr):
def __setattr__(self, attr, value):
raise AttributeError(
- "You can only write attribute on context object with the .set() method")
+ "You can only set attributes on context objects with the .set() method")
def set(self, **kw):
- state_id = _restore_ids.next()
+ state_id = next(_restore_ids)
try:
stack = self._local.stack
except AttributeError:
@@ -151,13 +151,19 @@ def __repr__(self):
self.__class__.__name__, myid, ' '.join(varlist))
-class RestoreState(object):
+class RestoreState:
def __init__(self, context, state_id):
self.state_id = state_id
self.context = context
self.restored = False
+ def __enter__(self):
+ return self.context
+
+ def __exit__(self, _exc_type, _exc_value, _exc_tb):
+ self.restore()
+
def restore(self):
if self.restored:
# @@: Should this really be allowed?
diff --git a/formencode/declarative.py b/src/formencode/declarative.py
similarity index 92%
rename from formencode/declarative.py
rename to src/formencode/declarative.py
index 504c44c9..550a2703 100644
--- a/formencode/declarative.py
+++ b/src/formencode/declarative.py
@@ -24,7 +24,7 @@
from itertools import count
-class classinstancemethod(object):
+class classinstancemethod:
"""
Acts like a class method when called from a class, like an
instance method when called by an instance. The method should
@@ -39,7 +39,7 @@ def __get__(self, obj, cls=None):
return _methodwrapper(self.func, obj=obj, cls=cls)
-class _methodwrapper(object):
+class _methodwrapper:
def __init__(self, func, obj, cls):
self.func = func
@@ -55,9 +55,9 @@ def __call__(self, *args, **kw):
def __repr__(self):
if self.obj is None:
return (''
- % (self.cls.__name__, self.func.func_name))
+ % (self.cls.__name__, self.func.__name__))
return (''
- % (self.cls.__name__, self.func.func_name, self.obj))
+ % (self.cls.__name__, self.func.__name__, self.obj))
class DeclarativeMeta(type):
@@ -66,11 +66,11 @@ def __new__(meta, class_name, bases, new_attrs):
cls = type.__new__(meta, class_name, bases, new_attrs)
for name in cls.__mutableattributes__:
setattr(cls, name, copy.copy(getattr(cls, name)))
- cls.declarative_count = cls.counter.next()
+ cls.declarative_count = next(cls.counter)
if ('__classinit__' in new_attrs and not isinstance(cls.__classinit__,
(staticmethod, types.FunctionType))):
setattr(cls, '__classinit__',
- staticmethod(cls.__classinit__.im_func))
+ staticmethod(cls.__classinit__.__func__))
cls.__classinit__(cls, new_attrs)
names = getattr(cls, '__singletonmethods__', None)
if names:
@@ -81,7 +81,7 @@ def __new__(meta, class_name, bases, new_attrs):
return cls
-class singletonmethod(object):
+class singletonmethod:
"""
For Declarative subclasses, this decorator will call the method
on the cls.singleton() object if called as a class method (or
@@ -97,14 +97,12 @@ def __get__(self, obj, cls=None):
return types.MethodType(self.func, obj)
-class Declarative(object):
+class Declarative(metaclass=DeclarativeMeta):
__unpackargs__ = ()
__mutableattributes__ = ()
- __metaclass__ = DeclarativeMeta
-
__singletonmethods__ = ()
counter = count()
@@ -143,10 +141,10 @@ def __init__(self, *args, **kw):
for name in self.__mutableattributes__:
if name not in kw:
setattr(self, name, copy.copy(getattr(self, name)))
- for name, value in kw.iteritems():
+ for name, value in kw.items():
setattr(self, name, value)
if 'declarative_count' not in kw:
- self.declarative_count = self.counter.next()
+ self.declarative_count = next(self.counter)
self.__initargs__(kw)
def __initargs__(self, new_attrs):
@@ -179,7 +177,7 @@ def __sourcerepr__(self, source, binding=None):
and self.__unpackargs__[1] in vals):
v = vals[self.__unpackargs__[1]]
if isinstance(v, (list, int)):
- args.extend(map(source.makeRepr, v))
+ args.extend(list(map(source.makeRepr, v)))
del v[self.__unpackargs__[1]]
for name in self.__unpackargs__:
if name in vals:
@@ -188,7 +186,7 @@ def __sourcerepr__(self, source, binding=None):
else:
break
args.extend('%s=%s' % (name, source.makeRepr(value))
- for (name, value) in vals.iteritems())
+ for (name, value) in vals.items())
return '%s(%s)' % (self.__class__.__name__,
', '.join(args))
diff --git a/formencode/doctest_xml_compare.py b/src/formencode/doctest_xml_compare.py
similarity index 93%
rename from formencode/doctest_xml_compare.py
rename to src/formencode/doctest_xml_compare.py
index 7f3a8bf9..395d697a 100644
--- a/formencode/doctest_xml_compare.py
+++ b/src/formencode/doctest_xml_compare.py
@@ -1,17 +1,14 @@
-
import doctest
import xml.etree.ElementTree as ET
-try:
- XMLParseError = ET.ParseError
-except AttributeError: # Python < 2.7
- from xml.parsers.expat import ExpatError as XMLParseError
+
+XMLParseError = ET.ParseError
RealOutputChecker = doctest.OutputChecker
def debug(*msg):
import sys
- print >> sys.stderr, ' '.join(map(str, msg))
+ print(' '.join(map(str, msg)), file=sys.stderr)
class HTMLOutputChecker(RealOutputChecker):
@@ -69,7 +66,7 @@ def xml_compare(x1, x2, reporter=None):
if reporter:
reporter('Tags do not match: %s and %s' % (x1.tag, x2.tag))
return False
- for name, value in x1.attrib.iteritems():
+ for name, value in x1.attrib.items():
if x2.attrib.get(name) != value:
if reporter:
reporter('Attributes do not match: %s=%r, %s=%r'
@@ -120,7 +117,7 @@ def make_xml(s):
def make_string(xml):
- if isinstance(xml, basestring):
+ if isinstance(xml, str):
xml = make_xml(xml)
s = ET.tostring(xml)
if s == '':
diff --git a/formencode/exc.py b/src/formencode/exc.py
similarity index 79%
rename from formencode/exc.py
rename to src/formencode/exc.py
index 8bc75298..3abdaf4a 100644
--- a/formencode/exc.py
+++ b/src/formencode/exc.py
@@ -2,7 +2,7 @@
Custom exceptions and warnings.
"""
-__all__ = ['FERunTimeWarning']
+__all__ = ['FERuntimeWarning']
class FERuntimeWarning(RuntimeWarning):
diff --git a/formencode/fieldstorage.py b/src/formencode/fieldstorage.py
similarity index 58%
rename from formencode/fieldstorage.py
rename to src/formencode/fieldstorage.py
index a581320a..93b2ccd8 100644
--- a/formencode/fieldstorage.py
+++ b/src/formencode/fieldstorage.py
@@ -1,11 +1,7 @@
-## FormEncode, a Form processor
-## Copyright (C) 2003, Ian Bicking
"""
Wrapper class for use with cgi.FieldStorage types for file uploads
"""
-import cgi
-
def convert_fieldstorage(fs):
return fs if fs.filename else None
diff --git a/formencode/foreach.py b/src/formencode/foreach.py
similarity index 90%
rename from formencode/foreach.py
rename to src/formencode/foreach.py
index b81e2957..96c064a1 100644
--- a/formencode/foreach.py
+++ b/src/formencode/foreach.py
@@ -14,13 +14,13 @@ class ForEach(CompoundValidator):
For instance::
- ForEach(AsInt(), InList([1, 2, 3]))
+ ForEach(Int(), OneOf([1, 2, 3]))
Will take a list of values and try to convert each of them to
an integer, and then check if each integer is 1, 2, or 3. Using
multiple arguments is equivalent to::
- ForEach(All(AsInt(), InList([1, 2, 3])))
+ ForEach(All(Int(), OneOf([1, 2, 3])))
Use convert_to_list=True if you want to force the input to be a
list. This will turn non-lists into one-element lists, and None
@@ -83,7 +83,7 @@ def _attempt_convert(self, value, state, validate):
new_list = set(new_list)
return new_list
else:
- raise Invalid('Errors:\n%s' % '\n'.join(unicode(e)
+ raise Invalid('Errors:\n%s' % '\n'.join(str(e)
for e in errors if e), value, state, error_list=errors)
finally:
if state is not None:
@@ -105,15 +105,14 @@ def _attempt_convert(self, value, state, validate):
def empty_value(self, value):
return []
- class _IfMissing(object):
+ class _IfMissing:
def __get__(self, obj, cls=None):
if obj is None:
return []
- elif obj._if_missing is ForEach._if_missing:
+ if obj._if_missing is ForEach._if_missing:
return []
- else:
- return obj._if_missing
+ return obj._if_missing
def __set__(self, obj, value):
obj._if_missing = value
@@ -125,7 +124,7 @@ def __delete__(self, obj):
del _IfMissing
def _convert_to_list(self, value):
- if isinstance(value, basestring):
+ if isinstance(value, str):
return [value]
elif value is None:
return []
@@ -135,6 +134,5 @@ def _convert_to_list(self, value):
for _n in value:
break
return value
- ## @@: Should this catch any other errors?:
- except TypeError:
+ except TypeError: # @@: Should this catch any other errors?
return [value]
diff --git a/formencode/htmlfill.py b/src/formencode/htmlfill.py
similarity index 90%
rename from formencode/htmlfill.py
rename to src/formencode/htmlfill.py
index c06ddc9b..a9fd12d9 100644
--- a/formencode/htmlfill.py
+++ b/src/formencode/htmlfill.py
@@ -17,7 +17,9 @@ def render(form, defaults=None, errors=None, use_all_keys=False,
text_as_default=False, checkbox_checked_if_present=False,
listener=None, encoding=None,
error_class='error', prefix_error=True,
- force_defaults=True, skip_passwords=False):
+ force_defaults=True, skip_passwords=False,
+ data_formencode_form=None, data_formencode_ignore=None,
+ ):
"""
Render the ``form`` (which should be a string) given the ``defaults``
and ``errors``. Defaults are the values that go in the input fields
@@ -66,8 +68,8 @@ def render(form, defaults=None, errors=None, use_all_keys=False,
``listener`` can be an object that watches fields pass; the only
one currently is in ``htmlfill_schemabuilder.SchemaBuilder``
- ``encoding`` specifies an encoding to assume when mixing str and
- unicode text in the template.
+ ``encoding`` specifies an encoding to assume when mixing bytes and
+ str text in the template.
``prefix_error`` specifies if the HTML created by auto_error_formatter is
put before the input control (default) or after the control.
@@ -83,6 +85,19 @@ def render(form, defaults=None, errors=None, use_all_keys=False,
rendering form-content. If disabled the password fields will not be filled
with anything, which is useful when you don't want to return a user's
password in plain-text source.
+
+ ``data_formencode_form`` if a string is passed in (default `None`) only
+ fields with the html attribute `data-formencode-form` that matches this
+ string will be processed. For example: if a HTML fragment has two forms they
+ can be differentiated to Formencode by decorating the input elements with
+ attributes such as `data-formencode-form="a"` or `data-formencode-form="b"`,
+ then instructing `render()` to only process the "a" or "b" fields.
+
+ ``data_formencode_ignore`` if True (default `None`) fields with the html
+ attribute `data-formencode-ignore` will not be processed. This attribute
+ need only be present in the tag: `data-formencode-ignore="1"`,
+ `data-formencode-ignore=""` and `data-formencode-ignore` without a value are
+ all valid signifiers.
"""
if defaults is None:
defaults = {}
@@ -101,13 +116,15 @@ def render(form, defaults=None, errors=None, use_all_keys=False,
error_class=error_class,
force_defaults=force_defaults,
skip_passwords=skip_passwords,
+ data_formencode_form=data_formencode_form,
+ data_formencode_ignore=data_formencode_ignore,
)
p.feed(form)
p.close()
return p.text()
-class htmlliteral(object):
+class htmlliteral:
def __init__(self, html, text=None):
if text is None:
@@ -189,7 +206,7 @@ class FillingParser(RewritingParser):
...
... ''')
>>> parser.close()
- >>> print parser.text() # doctest: +NORMALIZE_WHITESPACE
+ >>> print (parser.text()) # doctest: +NORMALIZE_WHITESPACE