From 06a1ab28db6b8d6632dfbed92160ecf76a45d9ea Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 14 Jan 2024 20:54:45 -0800 Subject: [PATCH] rewrite --- .editorconfig | 13 + .github/dependabot.yml | 18 ++ .github/workflows/lock.yaml | 23 ++ .github/workflows/publish.yaml | 68 +++++ .github/workflows/tests.yaml | 41 +++ .gitignore | 173 +----------- .pre-commit-config.yaml | 16 ++ .readthedocs.yaml | 13 + CHANGES.md | 46 ++++ LICENSE | 481 --------------------------------- LICENSE.txt | 20 ++ README.md | 59 +--- app/__init__.py | 50 ---- docs/_static/theme.css | 1 + docs/api.md | 10 + docs/changes.md | 4 + docs/conf.py | 53 ++++ docs/index.md | 54 ++++ docs/license.md | 5 + flask_ujson/__init__.py | 90 ------ pyproject.toml | 76 +++++- requirements.txt | 2 - requirements/build.in | 1 + requirements/build.txt | 12 + requirements/dev.in | 7 + requirements/dev.txt | 210 ++++++++++++++ requirements/docs.in | 3 + requirements/docs.txt | 78 ++++++ requirements/tests.in | 1 + requirements/tests.txt | 14 + requirements/typing.in | 3 + requirements/typing.txt | 22 ++ requirements_build.txt | 3 - src/flask_ujson/__init__.py | 5 + src/flask_ujson/provider.py | 72 +++++ src/flask_ujson/py.typed | 0 tests/conftest.py | 19 ++ tests/test_provider.py | 61 +++++ tox.ini | 43 +++ 39 files changed, 1021 insertions(+), 849 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/lock.yaml create mode 100644 .github/workflows/publish.yaml create mode 100644 .github/workflows/tests.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 CHANGES.md delete mode 100644 LICENSE create mode 100644 LICENSE.txt delete mode 100644 app/__init__.py create mode 100644 docs/_static/theme.css create mode 100644 docs/api.md create mode 100644 docs/changes.md create mode 100644 docs/conf.py create mode 100644 docs/index.md create mode 100644 docs/license.md delete mode 100644 flask_ujson/__init__.py delete mode 100644 requirements.txt create mode 100644 requirements/build.in create mode 100644 requirements/build.txt create mode 100644 requirements/dev.in create mode 100644 requirements/dev.txt create mode 100644 requirements/docs.in create mode 100644 requirements/docs.txt create mode 100644 requirements/tests.in create mode 100644 requirements/tests.txt create mode 100644 requirements/typing.in create mode 100644 requirements/typing.txt delete mode 100644 requirements_build.txt create mode 100644 src/flask_ujson/__init__.py create mode 100644 src/flask_ujson/provider.py create mode 100644 src/flask_ujson/py.typed create mode 100644 tests/conftest.py create mode 100644 tests/test_provider.py create mode 100644 tox.ini diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2ff985a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +charset = utf-8 +max_line_length = 88 + +[*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] +indent_size = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1f47f12 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + groups: + github-actions: + patterns: + - '*' + - package-ecosystem: pip + directory: /requirements/ + schedule: + interval: monthly + groups: + python-requirements: + patterns: + - '*' diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml new file mode 100644 index 0000000..be1e880 --- /dev/null +++ b/.github/workflows/lock.yaml @@ -0,0 +1,23 @@ +name: Lock inactive closed issues +# Lock closed issues that have not received any further activity for two weeks. +# This does not close open issues, only humans may do that. It is easier to +# respond to new issues with fresh examples rather than continuing discussions +# on old issues. + +on: + schedule: + - cron: '0 0 * * 0' +permissions: + issues: write + pull-requests: write +concurrency: + group: lock +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 + with: + issue-inactive-days: 14 + pr-inactive-days: 14 + discussion-inactive-days: 14 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..b257ef6 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,68 @@ +name: Publish +on: + push: + tags: + - '*' +jobs: + build: + runs-on: ubuntu-latest + outputs: + hash: ${{ steps.hash.outputs.hash }} + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c + with: + python-version: '3.x' + cache: pip + cache-dependency-path: requirements*/*.txt + - run: pip install -r requirements/build.txt + # Use the commit date instead of the current date during the build. + - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV + - run: python -m build + # Generate hashes used for provenance. + - name: generate hash + id: hash + run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT + - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 + with: + name: dist + path: ./dist + provenance: + needs: [build] + permissions: + actions: read + id-token: write + contents: write + # Can't pin with hash due to how this workflow works. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0 + with: + base64-subjects: ${{ needs.build.outputs.hash }} + create-release: + # Upload the sdist, wheels, and provenance to a GitHub release. They remain + # available as build artifacts for a while as well. + needs: [provenance] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a + - name: create release + run: > + gh release create --draft --repo ${{ github.repository }} + ${{ github.ref_name }} + *.intoto.jsonl/* dist/* + env: + GH_TOKEN: ${{ github.token }} + publish-pypi: + needs: [provenance] + # Wait for approval before attempting to upload to PyPI. This allows reviewing the + # files in the draft release. + environment: + name: publish + url: https://pypi.org/project/flask-ujson/ + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a + - uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..0a1f0f6 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,41 @@ +name: Tests +on: + push: + branches: + - main + - '*.x' + pull_request: +jobs: + tests: + name: ${{ matrix.python }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python: ['3.12', '3.11', '3.10', '3.9', '3.8'] + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + cache: pip + cache-dependency-path: requirements/*.txt + - run: pip install tox + - run: tox run -e py${{ matrix.python }} + typing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c + with: + python-version: '3.x' + cache: pip + cache-dependency-path: requirements/*.txt + - name: cache mypy + uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c + with: + path: ./.mypy_cache + key: mypy|${{ hashFiles('pyproject.toml') }} + - run: pip install tox + - run: tox run -e typing diff --git a/.gitignore b/.gitignore index b8fba39..39136db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,165 +1,10 @@ -### Python template -.idea/ -.ruff_cache/ - -# Byte-compiled / optimized / DLL files +/.idea/ +/.vscode/ +/.venv*/ +/venv*/ __pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - +/dist/ +/.coverage* +/htmlcov/ +/.tox/ +/docs/_build/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..81e76c2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +ci: + autoupdate_schedule: monthly +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.13 + hooks: + - id: ruff + - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-merge-conflict + - id: debug-statements + - id: fix-byte-order-marker + - id: trailing-whitespace + - id: end-of-file-fixer diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..865c685 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 +build: + os: ubuntu-22.04 + tools: + python: '3.12' +python: + install: + - requirements: requirements/docs.txt + - method: pip + path: . +sphinx: + builder: dirhtml + fail_on_warning: true diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..5c81841 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,46 @@ +## Version 2.0.0 + +Released 2024-01-14 + +- Simplify how the library is used and configured. +- The `UJSON` extension class is removed. Use `app.json = UjsonProvider(app)`. +- `UjsonProvider` has a `dump_args` attribute, a dict of default keyword + arguments to `ujson.dumps`. Keyword arguments to `dumps` overrides these. +- The provider does not have `sort_keys` or `compact` as arguments or + attributes. Use `dump_args` to set those arguments (and others) instead. +- The `__version__` attribute is removed. Call `importlib.metadata.version` instead. +- `datetime` and `date` objects use ISO 8601 format. +- Export type annotations. +- Change license to MIT. +- Use PyPI trusted publishing. +- Use `src` directory layout. + +## Version 1.0.6 + +Released 2023-12-01 + +## Version 1.0.5 + +Released 2023-12-01 + +## Version 1.0.4 + +Released 2023-11-30 + +## Version 1.0.3 + +Released 2023-11-30 + +## Version 1.0.2 + +Released 2023-11-30 + +## Version 1.0.1 + +Released 2023-11-30 + +## Version 1.0.0 + +Released 2023-11-30 + +- Initial release. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c8e1118..0000000 --- a/LICENSE +++ /dev/null @@ -1,481 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - -Flask-UJSON -A JSON provider for Flask using UltraJSON. - -Copyright (C) 2023 David Carmichael - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 -USA diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e4d8146 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2023 Pallets Community Ecosystem + +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/README.md b/README.md index ae1d600..3284722 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,19 @@ # flask-ujson -[![PyPI version](https://badge.fury.io/py/flask-ujson.svg)](https://badge.fury.io/py/flask-ujson) -[![License](https://img.shields.io/badge/license-LGPL_v2-red.svg)](https://raw.githubusercontent.com/CheeseCake87/flask-ujson/master/LICENSE) +A [Flask][]/[Quart][] JSON provider using the fast [ujson][] library. Using +this provider will significantly speed up reading JSON data in requests and +generating JSON responses. -`pip install flask-ujson` +[flask]: https://flask.palletsprojects.com +[quart]: https://quart.palletsprojects.com +[ujson]: https://github.com/ultrajson/ultrajson -Flask with UltraJSON. - -[https://github.com/ultrajson/ultrajson](https://github.com/ultrajson/ultrajson) +## Example ```python -from flask import Flask, request - -from flask_ujson import UJSON - -ultra_json = UJSON() - - -def create_app(): - app = Flask(__name__) - ultra_json.init_app(app) # Sets UltraJSON as the default JSON encoder - - @app.route("/") - def index(): - """ - Outputs a JSON response using UltraJSON library - - https://github.com/ultrajson/ultrajson - """ - return { - "timestamp": 1556283673.1523004, - "task_uuid": "0ed1a1c3-050c-4fb9-9426-a7e72d0acfc7", - "task_level": [1, 2, 1], - "action_status": "started", - "action_type": "main", - "key": "value", - "another_key": 123, - "and_another": ["a", "b"], - } - - @app.post("/post") - def accept_json(): - json = request.get_json() - return json - - return app - - -if __name__ == "__main__": - app = create_app() - app.run(debug=True) +from flask import Flask +from flask_ujson import UjsonProvider -``` \ No newline at end of file +app = Flask(__name__) +app.json = UjsonProvider(app) +``` diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index a24a511..0000000 --- a/app/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime - -from flask import Flask, request - -from flask_ujson import UJSON - -ultra_json = UJSON() - - -@dataclass -class NewDataClass: - hello: str = "world" - - -def create_app(): - app = Flask(__name__) - ultra_json.init_app(app) # Sets UltraJSON as the default JSON encoder - - @app.route("/") - def index(): - """ - Outputs a JSON response using UltraJSON library - - https://github.com/ultrajson/ultrajson - """ - return { - "timestamp": 1556283673.1523004, - "task_uuid": "0ed1a1c3-050c-4fb9-9426-a7e72d0acfc7", - "task_level": [1, 2, 1], - "action_status": "started", - "action_type": "main", - "key": "value", - "another_key": 123, - "and_another": ["a", "b"], - "today": datetime.now(), - "dataclass": NewDataClass() - } - - @app.post("/post") - def accept_json(): - json = request.get_json() - return json - - return app - - -if __name__ == "__main__": - app = create_app() - app.run(debug=True) diff --git a/docs/_static/theme.css b/docs/_static/theme.css new file mode 100644 index 0000000..35e705c --- /dev/null +++ b/docs/_static/theme.css @@ -0,0 +1 @@ +@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&family=Source+Code+Pro:ital,wght@0,400;0,700;1,400;1,700&display=swap'); diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..a2f2a6a --- /dev/null +++ b/docs/api.md @@ -0,0 +1,10 @@ +# API + +Anything documented here is part of the public API that flask-ujson provides, +unless otherwise indicated. Anything not documented here is considered internal +or private and may change at any time. + +```{eval-rst} +.. currentmodule:: flask_ujson +.. autoclass:: UjsonProvider +``` diff --git a/docs/changes.md b/docs/changes.md new file mode 100644 index 0000000..5f522c2 --- /dev/null +++ b/docs/changes.md @@ -0,0 +1,4 @@ +# Changes + +```{include} ../CHANGES.md +``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..4a00a4f --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,53 @@ +import importlib.metadata + +# Project -------------------------------------------------------------- + +project = "flask-ujson" +version = release = importlib.metadata.version("flask-ujson").partition(".dev")[0] + +# General -------------------------------------------------------------- + +default_role = "code" +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "myst_parser", +] +autodoc_member_order = "bysource" +autodoc_default_options = {"members": None} +autodoc_typehints = "description" +autodoc_preserve_defaults = True +extlinks = { + "issue": ("https://github.com/pallets-eco/flask-ujson/issues/%s", "#%s"), + "pr": ("https://github.com/pallets-eco/flask-ujson/pull/%s", "#%s"), +} +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "flask": ("https://flask.palletsprojects.com", None), +} +myst_enable_extensions = [ + "fieldlist", +] +myst_heading_anchors = 2 + +# HTML ----------------------------------------------------------------- + +html_theme = "furo" +html_static_path = ["_static"] +html_css_files = ["theme.css"] +html_copy_source = False +html_theme_options = { + "source_repository": "https://github.com/pallets-eco/flask-ujson/", + "source_branch": "main", + "source_directory": "docs/", + "light_css_variables": { + "font-stack": "'Atkinson Hyperlegible', sans-serif", + "font-stack--monospace": "'Source Code Pro', monospace", + }, +} +pygments_style = "default" +pygments_style_dark = "github-dark" +html_show_copyright = False +html_use_index = False +html_domain_indices = False diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6d73a00 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,54 @@ +# flask-ujson + +A [Flask][]/[Quart][] {class}`~.flask.json.provider.JSONProvider` using the fast +[ujson][] library. Using this provider will significantly speed up reading JSON +data in requests and generating JSON responses. + +[Flask]: https://flask.palletsprojects.com +[Quart]: https://quart.palletsprojects.com +[ujson]: https://github.com/ultrajson/ultrajson + +## Usage + +```python +from flask import Flask +from flask_ujson import UjsonProvider + +app = Flask(__name__) +json_provider = UjsonProvider(app) +app.json = json_provider +``` + +## Dump Arguments + +Ujson takes a number of options to control the behavior of `dumps`. Many of +these mirror the default {func}`json.dumps` arguments, and a few others are +described in the ujson docs. + +Flask-ujson sets the following defaults: + +- `ensure_ascii=False` +- `encode_html_chars=True` +- `default` converts the same types as Flask's default provider. However, it + uses ISO 8601 format for `datetime` and `date`. + +## Complex Data + +It's possible to set a `default` function in {attr}`~.UjsonProvider.dump_args` +to handle any data. However, we recommend using a dedicated object serialization +library to first convert complex data to JSON types, before serializing that to +JSON. This gives you full control over how your data is represented, as well as +the ability to deserialize data in requests. Some such libraries include +[cattrs][], [Marshmallow][], and [Pydantic][]. + +[cattrs]: https://catt.rs +[marshmallow]: https://marshmallow.readthedocs.io +[Pydantic]: https://docs.pydantic.dev + +```{toctree} +:hidden: + +api +changes +license +``` diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..0f433a0 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,5 @@ +# MIT License + +```{literalinclude} ../LICENSE.txt +:language: text +``` diff --git a/flask_ujson/__init__.py b/flask_ujson/__init__.py deleted file mode 100644 index 352bbfe..0000000 --- a/flask_ujson/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Flask with UltraJSON.""" - -import dataclasses -import decimal -import typing as t -import uuid -from datetime import date - -import ujson -from flask import Flask -from flask.json.provider import JSONProvider -from flask.wrappers import Response -from werkzeug.http import http_date - -__version__ = "1.0.6" - - -def _default(o: t.Any) -> t.Any: - if isinstance(o, date): - return http_date(o) - - if isinstance(o, (decimal.Decimal, uuid.UUID)): - return str(o) - - if dataclasses and dataclasses.is_dataclass(o): - return dataclasses.asdict(o) - - if hasattr(o, "__html__"): - return str(o.__html__()) - - raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable") - - -class UJSONProvider(JSONProvider): - """Provide JSON operations using the UltraJSON - library. Serializes the following additional data types: - - - :class:`datetime.datetime` and :class:`datetime.date` are - serialized to :rfc:`822` strings. This is the same as the HTTP - date format. - - :class:`uuid.UUID` is serialized to a string. - - :class:`dataclasses.dataclass` is passed to - :func:`dataclasses.asdict`. - - :class:`~markupsafe.Markup` (or any object with a ``__html__`` - method) will call the ``__html__`` method to get a string. - """ - - default: t.Callable[[t.Any], t.Any] = staticmethod( - _default - ) # type: ignore[assignment] - - ensure_ascii = True - sort_keys = True - compact: bool | None = None - mimetype = "application/json" - - def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: - kwargs.setdefault("default", self.default) - kwargs.setdefault("ensure_ascii", self.ensure_ascii) - kwargs.setdefault("sort_keys", self.sort_keys) - return ujson.dumps(obj, **kwargs) - - def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: - return ujson.loads(s, **kwargs) - - def response(self, *args: t.Any, **kwargs: t.Any) -> Response: - obj = self._prepare_response_obj(args, kwargs) - dump_args: dict[str, t.Any] = {} - - if (self.compact is None and self._app.debug) or self.compact is False: - dump_args.setdefault("indent", 2) - else: - dump_args.setdefault("separators", (",", ":")) - - return self._app.response_class( - f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype - ) - - -class UJSON: - def __init__(self, app: t.Optional[Flask] = None) -> None: - if app is not None: - self.init_app(app) - - def init_app(self, app: Flask) -> None: - if "ujson" in app.extensions: - raise RuntimeError("Flask app already initialized for ujson") - - app.extensions["ujson"] = self - app.json = UJSONProvider(app) diff --git a/pyproject.toml b/pyproject.toml index 724f1d9..9c32a90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,72 @@ -[build-system] -requires = ["flit_core >=3.2,<4"] -build-backend = "flit_core.buildapi" - [project] name = "flask-ujson" -authors = [{ name = "David Carmichael", email = "david@uilix.com" }] +version = "2.0.0" +description = "A Flask/Quart JSON provider using the fast ujson library." readme = "README.md" -license = { file = "LICENSE" } +license = { file = "LICENSE.txt" } +maintainers = [{ name = "Pallets Community Ecosystem", email = "contact@palletsprojects.com" }] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: Flask", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Typing :: Typed" +] requires-python = ">=3.8" -dynamic = ["version", "description"] dependencies = [ - "ujson >= 5.8.0", - "flask >= 3.0.0", + "flask", + "ujson", ] + [project.urls] -Home = "https://github.com/CheeseCake87/flask-ujson" +Documentation = "https://flask-ujson.readthedocs.io" +Changes = "https://flask-ujson.readthedocs.io/changes/" +Source = "https://github.com/pallets-eco/flask-ujson/" + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = [ + "error", +] + +[tool.coverage.run] +branch = true +source = ["flask_ujson", "tests"] + +[tool.coverage.paths] +source = ["src", "*/site-packages"] + +[tool.mypy] +files = ["src/flask_ujson", "tests"] +show_error_codes = true +pretty = true +strict = true + +[tool.pyright] +include = ["src/flask_ujson", "tests"] + +[tool.ruff] +src = ["src"] +fix = true +unsafe-fixes = true +show-fixes = true +show-source = true + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "E", # pycodestyle error + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "W", # pycodestyle warning +] +ignore-init-module-imports = true -[tool.flit.sdist] -exclude = ["app/", ".gitignore", "requirements_build.txt"] \ No newline at end of file +[tool.ruff.lint.isort] +force-single-line = true +order-by-type = false diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6de8f66..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -flask -ujson diff --git a/requirements/build.in b/requirements/build.in new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/requirements/build.in @@ -0,0 +1 @@ +build diff --git a/requirements/build.txt b/requirements/build.txt new file mode 100644 index 0000000..fc64040 --- /dev/null +++ b/requirements/build.txt @@ -0,0 +1,12 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile build.in +# +build==1.0.3 + # via -r build.in +packaging==23.2 + # via build +pyproject-hooks==1.0.0 + # via build diff --git a/requirements/dev.in b/requirements/dev.in new file mode 100644 index 0000000..a234380 --- /dev/null +++ b/requirements/dev.in @@ -0,0 +1,7 @@ +-r docs.txt +-r tests.txt +-r typing.txt +pip-tools +ruff +pre-commit +tox diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..d97639b --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,210 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile dev.in +# +alabaster==0.7.16 + # via + # -r docs.txt + # sphinx +babel==2.14.0 + # via + # -r docs.txt + # sphinx +beautifulsoup4==4.12.2 + # via + # -r docs.txt + # furo +build==1.0.3 + # via pip-tools +cachetools==5.3.2 + # via tox +certifi==2023.11.17 + # via + # -r docs.txt + # requests +cfgv==3.4.0 + # via pre-commit +chardet==5.2.0 + # via tox +charset-normalizer==3.3.2 + # via + # -r docs.txt + # requests +click==8.1.7 + # via pip-tools +colorama==0.4.6 + # via tox +distlib==0.3.8 + # via virtualenv +docutils==0.20.1 + # via + # -r docs.txt + # myst-parser + # sphinx +filelock==3.13.1 + # via + # tox + # virtualenv +furo==2023.9.10 + # via -r docs.txt +identify==2.5.33 + # via pre-commit +idna==3.6 + # via + # -r docs.txt + # requests +imagesize==1.4.1 + # via + # -r docs.txt + # sphinx +iniconfig==2.0.0 + # via + # -r tests.txt + # -r typing.txt + # pytest +jinja2==3.1.3 + # via + # -r docs.txt + # myst-parser + # sphinx +markdown-it-py==3.0.0 + # via + # -r docs.txt + # mdit-py-plugins + # myst-parser +markupsafe==2.1.3 + # via + # -r docs.txt + # jinja2 +mdit-py-plugins==0.4.0 + # via + # -r docs.txt + # myst-parser +mdurl==0.1.2 + # via + # -r docs.txt + # markdown-it-py +mypy==1.8.0 + # via -r typing.txt +mypy-extensions==1.0.0 + # via + # -r typing.txt + # mypy +myst-parser==2.0.0 + # via -r docs.txt +nodeenv==1.8.0 + # via pre-commit +packaging==23.2 + # via + # -r docs.txt + # -r tests.txt + # -r typing.txt + # build + # pyproject-api + # pytest + # sphinx + # tox +pip-tools==7.3.0 + # via -r dev.in +platformdirs==4.1.0 + # via + # tox + # virtualenv +pluggy==1.3.0 + # via + # -r tests.txt + # -r typing.txt + # pytest + # tox +pre-commit==3.6.0 + # via -r dev.in +pygments==2.17.2 + # via + # -r docs.txt + # furo + # sphinx +pyproject-api==1.6.1 + # via tox +pyproject-hooks==1.0.0 + # via build +pytest==7.4.4 + # via + # -r tests.txt + # -r typing.txt +pyyaml==6.0.1 + # via + # -r docs.txt + # myst-parser + # pre-commit +requests==2.31.0 + # via + # -r docs.txt + # sphinx +ruff==0.1.13 + # via -r dev.in +snowballstemmer==2.2.0 + # via + # -r docs.txt + # sphinx +soupsieve==2.5 + # via + # -r docs.txt + # beautifulsoup4 +sphinx==7.2.6 + # via + # -r docs.txt + # furo + # myst-parser + # sphinx-basic-ng +sphinx-basic-ng==1.0.0b2 + # via + # -r docs.txt + # furo +sphinxcontrib-applehelp==1.0.8 + # via + # -r docs.txt + # sphinx +sphinxcontrib-devhelp==1.0.6 + # via + # -r docs.txt + # sphinx +sphinxcontrib-htmlhelp==2.0.5 + # via + # -r docs.txt + # sphinx +sphinxcontrib-jsmath==1.0.1 + # via + # -r docs.txt + # sphinx +sphinxcontrib-qthelp==1.0.7 + # via + # -r docs.txt + # sphinx +sphinxcontrib-serializinghtml==1.1.10 + # via + # -r docs.txt + # sphinx +tox==4.12.0 + # via -r dev.in +types-ujson==5.9.0.0 + # via -r typing.txt +typing-extensions==4.9.0 + # via + # -r typing.txt + # mypy +urllib3==2.1.0 + # via + # -r docs.txt + # requests +virtualenv==20.25.0 + # via + # pre-commit + # tox +wheel==0.42.0 + # via pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements/docs.in b/requirements/docs.in new file mode 100644 index 0000000..db72b7b --- /dev/null +++ b/requirements/docs.in @@ -0,0 +1,3 @@ +sphinx +myst-parser +furo diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 0000000..4e26e4d --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,78 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile docs.in +# +alabaster==0.7.16 + # via sphinx +babel==2.14.0 + # via sphinx +beautifulsoup4==4.12.2 + # via furo +certifi==2023.11.17 + # via requests +charset-normalizer==3.3.2 + # via requests +docutils==0.20.1 + # via + # myst-parser + # sphinx +furo==2023.9.10 + # via -r docs.in +idna==3.6 + # via requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.3 + # via + # myst-parser + # sphinx +markdown-it-py==3.0.0 + # via + # mdit-py-plugins + # myst-parser +markupsafe==2.1.3 + # via jinja2 +mdit-py-plugins==0.4.0 + # via myst-parser +mdurl==0.1.2 + # via markdown-it-py +myst-parser==2.0.0 + # via -r docs.in +packaging==23.2 + # via sphinx +pygments==2.17.2 + # via + # furo + # sphinx +pyyaml==6.0.1 + # via myst-parser +requests==2.31.0 + # via sphinx +snowballstemmer==2.2.0 + # via sphinx +soupsieve==2.5 + # via beautifulsoup4 +sphinx==7.2.6 + # via + # -r docs.in + # furo + # myst-parser + # sphinx-basic-ng +sphinx-basic-ng==1.0.0b2 + # via furo +sphinxcontrib-applehelp==1.0.8 + # via sphinx +sphinxcontrib-devhelp==1.0.6 + # via sphinx +sphinxcontrib-htmlhelp==2.0.5 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.7 + # via sphinx +sphinxcontrib-serializinghtml==1.1.10 + # via sphinx +urllib3==2.1.0 + # via requests diff --git a/requirements/tests.in b/requirements/tests.in new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/requirements/tests.in @@ -0,0 +1 @@ +pytest diff --git a/requirements/tests.txt b/requirements/tests.txt new file mode 100644 index 0000000..30d1ce9 --- /dev/null +++ b/requirements/tests.txt @@ -0,0 +1,14 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile tests.in +# +iniconfig==2.0.0 + # via pytest +packaging==23.2 + # via pytest +pluggy==1.3.0 + # via pytest +pytest==7.4.4 + # via -r tests.in diff --git a/requirements/typing.in b/requirements/typing.in new file mode 100644 index 0000000..a051432 --- /dev/null +++ b/requirements/typing.in @@ -0,0 +1,3 @@ +mypy +pytest +types-ujson diff --git a/requirements/typing.txt b/requirements/typing.txt new file mode 100644 index 0000000..709632e --- /dev/null +++ b/requirements/typing.txt @@ -0,0 +1,22 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile typing.in +# +iniconfig==2.0.0 + # via pytest +mypy==1.8.0 + # via -r typing.in +mypy-extensions==1.0.0 + # via mypy +packaging==23.2 + # via pytest +pluggy==1.3.0 + # via pytest +pytest==7.4.4 + # via -r typing.in +types-ujson==5.9.0.0 + # via -r typing.in +typing-extensions==4.9.0 + # via mypy diff --git a/requirements_build.txt b/requirements_build.txt deleted file mode 100644 index 97fe208..0000000 --- a/requirements_build.txt +++ /dev/null @@ -1,3 +0,0 @@ --r requirements.txt -flit -ruff \ No newline at end of file diff --git a/src/flask_ujson/__init__.py b/src/flask_ujson/__init__.py new file mode 100644 index 0000000..b1079b9 --- /dev/null +++ b/src/flask_ujson/__init__.py @@ -0,0 +1,5 @@ +from .provider import UjsonProvider + +__all__ = [ + "UjsonProvider", +] diff --git a/src/flask_ujson/provider.py b/src/flask_ujson/provider.py new file mode 100644 index 0000000..565a646 --- /dev/null +++ b/src/flask_ujson/provider.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import dataclasses +import typing as t +from datetime import date +from datetime import datetime +from datetime import timezone +from uuid import UUID + +import ujson +from flask.json.provider import JSONProvider +from flask.sansio.app import App + + +def _default(o: t.Any) -> t.Any: + if isinstance(o, datetime): + if o.tzinfo is None: + o = o.replace(tzinfo=timezone.utc) + + return o.isoformat() + + if isinstance(o, date): + return o.isoformat() + + if isinstance(o, UUID): + return str(o) + + if dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + + if hasattr(o, "__html__"): + return str(o.__html__()) + + raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable") + + +class UjsonProvider(JSONProvider): + """A :class:`~flask.json.provider.JSONProvider` that uses the fast + `ujson `__ library. + """ + + dump_args: dict[str, t.Any] = { + "ensure_ascii": False, + "encode_html_chars": True, + "default": _default, + } + """Default keyword arguments passed to ``ujson.dumps``.""" + + def __init__(self, app: App) -> None: + super().__init__(app) + self.dump_args = self.dump_args.copy() + + def dumps( + self, + obj: t.Any, + **kwargs: t.Any, + ) -> str: + """Serialize data as JSON. + + :param obj: The data to serialize. + :param kwargs: Arguments passed to ``ujson.dumps``. Overrides defaults + set by :attr:`dump_args`. + """ + return ujson.dumps(obj, **self.dump_args, **kwargs) + + def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON. + + :param s: Text or UTF-8 bytes. + :param kwargs: All keyword arguments are silently ignored. + """ + return ujson.loads(s) diff --git a/src/flask_ujson/py.typed b/src/flask_ujson/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7c6c0f6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import pytest +from flask import Flask +from flask.testing import FlaskClient + +from flask_ujson import UjsonProvider + + +@pytest.fixture +def app() -> Flask: + app = Flask(__name__) + app.json = UjsonProvider(app) + return app + + +@pytest.fixture +def client(app: Flask) -> FlaskClient: + return app.test_client() diff --git a/tests/test_provider.py b/tests/test_provider.py new file mode 100644 index 0000000..451f557 --- /dev/null +++ b/tests/test_provider.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import typing as t +from dataclasses import dataclass +from datetime import date +from datetime import datetime +from datetime import timedelta +from datetime import timezone +from decimal import Decimal +from uuid import UUID + +import pytest +from flask import Flask +from flask import request +from flask.testing import FlaskClient + + +def test_request_response(app: Flask, client: FlaskClient) -> None: + @app.post("/") + def echo() -> t.Any: + return request.json + + class User: + def __init__(self, name: str) -> None: + self.name = name + + def __html__(self) -> str: + return f"{self.name}" + + @dataclass + class Roll: + count: int + sides: int + + pst = timezone(timedelta(hours=-8), "PST") + rv = client.post( + "/", + json={ + "datetime-naive": datetime(2024, 1, 12, 9, 42), + "datetime-aware": datetime(2024, 1, 12, 9, 42, tzinfo=pst), + "date": date(2024, 1, 12), + "decimal": Decimal("3.14159"), + "uuid": UUID("d0086ec3-d2f8-4bd9-9602-dae3ffabc0da"), + "dataclass": Roll(1, 20), + "html": User("flask"), + }, + ) + assert rv.json == { + "datetime-naive": "2024-01-12T09:42:00+00:00", + "datetime-aware": "2024-01-12T09:42:00-08:00", + "date": "2024-01-12", + "decimal": 3.14159, + "uuid": "d0086ec3-d2f8-4bd9-9602-dae3ffabc0da", + "dataclass": {"count": 1, "sides": 20}, + "html": "flask", + } + + +def test_default_unsupported(app: Flask) -> None: + with pytest.raises(TypeError): + app.json.dumps({"a": ...}) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..627883d --- /dev/null +++ b/tox.ini @@ -0,0 +1,43 @@ +[tox] +envlist = + py3{12,11,10,9,8} + style + typing + docs +skip_missing_interpreters = true + +[testenv] +package = wheel +wheel_build_env = .pkg +envtmpdir = {toxworkdir}/tmp/{envname} +constrain_package_deps = true +use_frozen_constraints = true +deps = -r requirements/tests.txt +commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs} + +[testenv:style] +deps = pre-commit +skip_install = true +commands = pre-commit run --all-files + +[testenv:typing] +deps = -r requirements/typing.txt +commands = mypy + +[testenv:docs] +deps = -r requirements/docs.txt +commands = sphinx-build -W -b dirhtml docs docs/_build/dirhtml + +[testenv:update-requirements] +deps = + pip-tools + pre-commit +skip_install = true +change_dir = requirements +commands = + pre-commit autoupdate -j4 + pip-compile -U build.in + pip-compile -U docs.in + pip-compile -U tests.in + pip-compile -U typing.in + pip-compile -U dev.in