From 820240c9d7a7ff2b0c0fcdd71b5f6acfa851ad16 Mon Sep 17 00:00:00 2001 From: brentru Date: Wed, 27 Jan 2021 15:57:25 -0500 Subject: [PATCH] Initial commit! --- .github/workflows/build.yml | 81 ++ .github/workflows/release.yml | 85 ++ .gitignore | 18 + .pre-commit-config.yaml | 19 + .pylintrc | 437 ++++++++ .readthedocs.yml | 7 + CODE_OF_CONDUCT.md | 137 +++ LICENSE | 21 + LICENSES/CC-BY-4.0.txt | 324 ++++++ LICENSES/MIT.txt | 19 + LICENSES/Python-2.0.txt | 279 +++++ LICENSES/Unlicense.txt | 20 + README.rst | 110 ++ README.rst.license | 3 + adafruit_datetime.py | 1723 ++++++++++++++++++++++++++++++ docs/_static/favicon.ico | Bin 0 -> 4414 bytes docs/_static/favicon.ico.license | 3 + docs/api.rst | 8 + docs/api.rst.license | 3 + docs/conf.py | 187 ++++ docs/examples.rst | 8 + docs/examples.rst.license | 3 + docs/index.rst | 47 + docs/index.rst.license | 3 + examples/datetime_simpletest.py | 36 + examples/datetime_time.py | 30 + examples/datetime_timedelta.py | 29 + pyproject.toml | 6 + requirements.txt | 6 + setup.py | 61 ++ tests/test_date.py | 458 ++++++++ tests/test_datetime.py | 1244 +++++++++++++++++++++ tests/test_time.py | 386 +++++++ 33 files changed, 5801 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .pylintrc create mode 100644 .readthedocs.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 LICENSES/CC-BY-4.0.txt create mode 100644 LICENSES/MIT.txt create mode 100644 LICENSES/Python-2.0.txt create mode 100644 LICENSES/Unlicense.txt create mode 100644 README.rst create mode 100644 README.rst.license create mode 100644 adafruit_datetime.py create mode 100644 docs/_static/favicon.ico create mode 100644 docs/_static/favicon.ico.license create mode 100644 docs/api.rst create mode 100644 docs/api.rst.license create mode 100644 docs/conf.py create mode 100644 docs/examples.rst create mode 100644 docs/examples.rst.license create mode 100644 docs/index.rst create mode 100644 docs/index.rst.license create mode 100644 examples/datetime_simpletest.py create mode 100644 examples/datetime_time.py create mode 100644 examples/datetime_timedelta.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/test_date.py create mode 100644 tests/test_datetime.py create mode 100644 tests/test_time.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d31bb47 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: Build CI + +on: [pull_request, push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Translate Repo Name For Build Tools filename_prefix + id: repo-name + run: | + echo ::set-output name=repo-name::$( + echo ${{ github.repository }} | + awk -F '\/' '{ print tolower($2) }' | + tr '_' '-' + ) + - name: Set up Python 3.6 + uses: actions/setup-python@v1 + with: + python-version: 3.6 + - name: Versions + run: | + python3 --version + - name: Checkout Current Repo + uses: actions/checkout@v1 + with: + submodules: true + - name: Checkout tools repo + uses: actions/checkout@v2 + with: + repository: adafruit/actions-ci-circuitpython-libs + path: actions-ci + - name: Install dependencies + # (e.g. - apt-get: gettext, etc; pip: circuitpython-build-tools, requirements.txt; etc.) + run: | + source actions-ci/install.sh + - name: Pip install pylint, Sphinx, pre-commit + run: | + pip install --force-reinstall pylint Sphinx sphinx-rtd-theme pre-commit + - name: Library version + run: git describe --dirty --always --tags + - name: Pre-commit hooks + run: | + pre-commit run --all-files + - name: PyLint + run: | + pylint $( find . -path './adafruit*.py' ) + ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace $( find . -path "./examples/*.py" )) + - name: Run tests + run: | + cd tests/ && python -m unittest discover + cd .. + - name: Build assets + run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . + - name: Archive bundles + uses: actions/upload-artifact@v2 + with: + name: bundles + path: ${{ github.workspace }}/bundles/ + - name: Build docs + working-directory: docs + run: sphinx-build -E -W -b html . _build/html + - name: Check For setup.py + id: need-pypi + run: | + echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) + - name: Build Python package + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + run: | + pip install --upgrade setuptools wheel twine readme_renderer testresources + python setup.py sdist + python setup.py bdist_wheel --universal + twine check dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6d0015a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: Release Actions + +on: + release: + types: [published] + +jobs: + upload-release-assets: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Translate Repo Name For Build Tools filename_prefix + id: repo-name + run: | + echo ::set-output name=repo-name::$( + echo ${{ github.repository }} | + awk -F '\/' '{ print tolower($2) }' | + tr '_' '-' + ) + - name: Set up Python 3.6 + uses: actions/setup-python@v1 + with: + python-version: 3.6 + - name: Versions + run: | + python3 --version + - name: Checkout Current Repo + uses: actions/checkout@v1 + with: + submodules: true + - name: Checkout tools repo + uses: actions/checkout@v2 + with: + repository: adafruit/actions-ci-circuitpython-libs + path: actions-ci + - name: Install deps + run: | + source actions-ci/install.sh + - name: Build assets + run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . + - name: Upload Release Assets + # the 'official' actions version does not yet support dynamically + # supplying asset names to upload. @csexton's version chosen based on + # discussion in the issue below, as its the simplest to implement and + # allows for selecting files with a pattern. + # https://github.com/actions/upload-release-asset/issues/4 + #uses: actions/upload-release-asset@v1.0.1 + uses: csexton/release-asset-action@master + with: + pattern: "bundles/*" + github-token: ${{ secrets.GITHUB_TOKEN }} + + upload-pypi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Check For setup.py + id: need-pypi + run: | + echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) + - name: Set up Python + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + env: + TWINE_USERNAME: ${{ secrets.pypi_username }} + TWINE_PASSWORD: ${{ secrets.pypi_password }} + run: | + python setup.py sdist + twine upload dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c6ddfd --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +*.mpy +.idea +__pycache__ +_build +*.pyc +.env +.python-version +build*/ +bundles +*.DS_Store +.eggs +dist +**/*.egg-info +.vscode diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..aab5f1c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò +# +# SPDX-License-Identifier: Unlicense + +repos: +- repo: https://github.com/python/black + rev: stable + hooks: + - id: black +- repo: https://github.com/fsfe/reuse-tool + rev: latest + hooks: + - id: reuse +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..5ce165c --- /dev/null +++ b/.pylintrc @@ -0,0 +1,437 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the ignore-list. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +# jobs=1 +jobs=2 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,bad-continuation,pointless-string-statement + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +# notes=FIXME,XXX,TODO +notes=FIXME,XXX + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules=board + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +# expected-line-ending-format= +expected-line-ending-format=LF + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming hint for argument names +argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct argument names +argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for attribute names +attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct attribute names +attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class names +# class-name-hint=[A-Z_][a-zA-Z0-9]+$ +class-name-hint=[A-Z_][a-zA-Z0-9_]+$ + +# Regular expression matching correct class names +# class-rgx=[A-Z_][a-zA-Z0-9]+$ +class-rgx=[A-Z_][a-zA-Z0-9_]+$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming hint for function names +function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct function names +function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Good variable names which should always be accepted, separated by a comma +# good-names=i,j,k,ex,Run,_ +good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for method names +method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct method names +method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming hint for variable names +variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +# max-attributes=7 +max-attributes=11 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..a1e2575 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +python: + version: 3 +requirements_file: requirements.txt diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d885b36 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,137 @@ + +# Adafruit Community Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and leaders pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level or type of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +We are committed to providing a friendly, safe and welcoming environment for +all. + +Examples of behavior that contributes to creating a positive environment +include: + +* Be kind and courteous to others +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Collaborating with other community members +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and sexual attention or advances +* The use of inappropriate images, including in a community member's avatar +* The use of inappropriate language, including in a community member's nickname +* Any spamming, flaming, baiting or other attention-stealing behavior +* Excessive or unwelcome helping; answering outside the scope of the question + asked +* Trolling, insulting/derogatory comments, and personal or political attacks +* Promoting or spreading disinformation, lies, or conspiracy theories against + a person, group, organisation, project, or community +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate + +The goal of the standards and moderation guidelines outlined here is to build +and maintain a respectful community. We ask that you don’t just aim to be +"technically unimpeachable", but rather try to be your best self. + +We value many things beyond technical expertise, including collaboration and +supporting others within our community. Providing a positive experience for +other community members can have a much more significant impact than simply +providing the correct answer. + +## Our Responsibilities + +Project leaders are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project leaders have the right and responsibility to remove, edit, or +reject messages, comments, commits, code, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any community member for other behaviors that they deem +inappropriate, threatening, offensive, or harmful. + +## Moderation + +Instances of behaviors that violate the Adafruit Community Code of Conduct +may be reported by any member of the community. Community members are +encouraged to report these situations, including situations they witness +involving other community members. + +You may report in the following ways: + +In any situation, you may send an email to . + +On the Adafruit Discord, you may send an open message from any channel +to all Community Moderators by tagging @community moderators. You may +also send an open message from any channel, or a direct message to +@kattni#1507, @tannewt#4653, @danh#1614, @cater#2442, +@sommersoft#0222, @Mr. Certainly#0472 or @Andon#8175. + +Email and direct message reports will be kept confidential. + +In situations on Discord where the issue is particularly egregious, possibly +illegal, requires immediate action, or violates the Discord terms of service, +you should also report the message directly to Discord. + +These are the steps for upholding our community’s standards of conduct. + +1. Any member of the community may report any situation that violates the +Adafruit Community Code of Conduct. All reports will be reviewed and +investigated. +2. If the behavior is an egregious violation, the community member who +committed the violation may be banned immediately, without warning. +3. Otherwise, moderators will first respond to such behavior with a warning. +4. Moderators follow a soft "three strikes" policy - the community member may +be given another chance, if they are receptive to the warning and change their +behavior. +5. If the community member is unreceptive or unreasonable when warned by a +moderator, or the warning goes unheeded, they may be banned for a first or +second offense. Repeated offenses will result in the community member being +banned. + +## Scope + +This Code of Conduct and the enforcement policies listed above apply to all +Adafruit Community venues. This includes but is not limited to any community +spaces (both public and private), the entire Adafruit Discord server, and +Adafruit GitHub repositories. Examples of Adafruit Community spaces include +but are not limited to meet-ups, audio chats on the Adafruit Discord, or +interaction at a conference. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. As a community +member, you are representing our community, and are expected to behave +accordingly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant], +version 1.4, available at +, +and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). + +For other projects adopting the Adafruit Community Code of +Conduct, please contact the maintainers of those projects for enforcement. +If you wish to use this code of conduct for your own project, consider +explicitly mentioning your moderation policy or making a copy with your +own moderation policy so as to avoid confusion. + +[Contributor Covenant]: https://www.contributor-covenant.org diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0b2da59 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Brent Rubell for Adafruit Industries + +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/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt new file mode 100644 index 0000000..3f92dfc --- /dev/null +++ b/LICENSES/CC-BY-4.0.txt @@ -0,0 +1,324 @@ +Creative Commons Attribution 4.0 International Creative Commons Corporation +("Creative Commons") is not a law firm and does not provide legal services +or legal advice. Distribution of Creative Commons public licenses does not +create a lawyer-client or other relationship. Creative Commons makes its licenses +and related information available on an "as-is" basis. Creative Commons gives +no warranties regarding its licenses, any material licensed under their terms +and conditions, or any related information. Creative Commons disclaims all +liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions +that creators and other rights holders may use to share original works of +authorship and other material subject to copyright and certain other rights +specified in the public license below. The following considerations are for +informational purposes only, are not exhaustive, and do not form part of our +licenses. + +Considerations for licensors: Our public licenses are intended for use by +those authorized to give the public permission to use material in ways otherwise +restricted by copyright and certain other rights. Our licenses are irrevocable. +Licensors should read and understand the terms and conditions of the license +they choose before applying it. Licensors should also secure all rights necessary +before applying our licenses so that the public can reuse the material as +expected. Licensors should clearly mark any material not subject to the license. +This includes other CC-licensed material, or material used under an exception +or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors + +Considerations for the public: By using one of our public licenses, a licensor +grants the public permission to use the licensed material under specified +terms and conditions. If the licensor's permission is not necessary for any +reason–for example, because of any applicable exception or limitation to copyright–then +that use is not regulated by the license. Our licenses grant only permissions +under copyright and certain other rights that a licensor has authority to +grant. Use of the licensed material may still be restricted for other reasons, +including because others have copyright or other rights in the material. A +licensor may make special requests, such as asking that all changes be marked +or described. Although not required by our licenses, you are encouraged to +respect those requests where reasonable. More considerations for the public +: wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution +4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to +be bound by the terms and conditions of this Creative Commons Attribution +4.0 International Public License ("Public License"). To the extent this Public +License may be interpreted as a contract, You are granted the Licensed Rights +in consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the Licensor +receives from making the Licensed Material available under these terms and +conditions. + +Section 1 – Definitions. + +a. Adapted Material means material subject to Copyright and Similar Rights +that is derived from or based upon the Licensed Material and in which the +Licensed Material is translated, altered, arranged, transformed, or otherwise +modified in a manner requiring permission under the Copyright and Similar +Rights held by the Licensor. For purposes of this Public License, where the +Licensed Material is a musical work, performance, or sound recording, Adapted +Material is always produced where the Licensed Material is synched in timed +relation with a moving image. + +b. Adapter's License means the license You apply to Your Copyright and Similar +Rights in Your contributions to Adapted Material in accordance with the terms +and conditions of this Public License. + +c. Copyright and Similar Rights means copyright and/or similar rights closely +related to copyright including, without limitation, performance, broadcast, +sound recording, and Sui Generis Database Rights, without regard to how the +rights are labeled or categorized. For purposes of this Public License, the +rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + +d. Effective Technological Measures means those measures that, in the absence +of proper authority, may not be circumvented under laws fulfilling obligations +under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, +and/or similar international agreements. + +e. Exceptions and Limitations means fair use, fair dealing, and/or any other +exception or limitation to Copyright and Similar Rights that applies to Your +use of the Licensed Material. + +f. Licensed Material means the artistic or literary work, database, or other +material to which the Licensor applied this Public License. + +g. Licensed Rights means the rights granted to You subject to the terms and +conditions of this Public License, which are limited to all Copyright and +Similar Rights that apply to Your use of the Licensed Material and that the +Licensor has authority to license. + +h. Licensor means the individual(s) or entity(ies) granting rights under this +Public License. + +i. Share means to provide material to the public by any means or process that +requires permission under the Licensed Rights, such as reproduction, public +display, public performance, distribution, dissemination, communication, or +importation, and to make material available to the public including in ways +that members of the public may access the material from a place and at a time +individually chosen by them. + +j. Sui Generis Database Rights means rights other than copyright resulting +from Directive 96/9/EC of the European Parliament and of the Council of 11 +March 1996 on the legal protection of databases, as amended and/or succeeded, +as well as other essentially equivalent rights anywhere in the world. + +k. You means the individual or entity exercising the Licensed Rights under +this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + +1. Subject to the terms and conditions of this Public License, the Licensor +hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, +irrevocable license to exercise the Licensed Rights in the Licensed Material +to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + +2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions +and Limitations apply to Your use, this Public License does not apply, and +You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + +4. Media and formats; technical modifications allowed. The Licensor authorizes +You to exercise the Licensed Rights in all media and formats whether now known +or hereafter created, and to make technical modifications necessary to do +so. The Licensor waives and/or agrees not to assert any right or authority +to forbid You from making technical modifications necessary to exercise the +Licensed Rights, including technical modifications necessary to circumvent +Effective Technological Measures. For purposes of this Public License, simply +making modifications authorized by this Section 2(a)(4) never produces Adapted +Material. + + 5. Downstream recipients. + +A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed +Material automatically receives an offer from the Licensor to exercise the +Licensed Rights under the terms and conditions of this Public License. + +B. No downstream restrictions. You may not offer or impose any additional +or different terms or conditions on, or apply any Effective Technological +Measures to, the Licensed Material if doing so restricts exercise of the Licensed +Rights by any recipient of the Licensed Material. + +6. No endorsement. Nothing in this Public License constitutes or may be construed +as permission to assert or imply that You are, or that Your use of the Licensed +Material is, connected with, or sponsored, endorsed, or granted official status +by, the Licensor or others designated to receive attribution as provided in +Section 3(a)(1)(A)(i). + + b. Other rights. + +1. Moral rights, such as the right of integrity, are not licensed under this +Public License, nor are publicity, privacy, and/or other similar personality +rights; however, to the extent possible, the Licensor waives and/or agrees +not to assert any such rights held by the Licensor to the limited extent necessary +to allow You to exercise the Licensed Rights, but not otherwise. + +2. Patent and trademark rights are not licensed under this Public License. + +3. To the extent possible, the Licensor waives any right to collect royalties +from You for the exercise of the Licensed Rights, whether directly or through +a collecting society under any voluntary or waivable statutory or compulsory +licensing scheme. In all other cases the Licensor expressly reserves any right +to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following +conditions. + + a. Attribution. + +1. If You Share the Licensed Material (including in modified form), You must: + +A. retain the following if it is supplied by the Licensor with the Licensed +Material: + +i. identification of the creator(s) of the Licensed Material and any others +designated to receive attribution, in any reasonable manner requested by the +Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + +v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + +B. indicate if You modified the Licensed Material and retain an indication +of any previous modifications; and + +C. indicate the Licensed Material is licensed under this Public License, and +include the text of, or the URI or hyperlink to, this Public License. + +2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner +based on the medium, means, and context in which You Share the Licensed Material. +For example, it may be reasonable to satisfy the conditions by providing a +URI or hyperlink to a resource that includes the required information. + +3. If requested by the Licensor, You must remove any of the information required +by Section 3(a)(1)(A) to the extent reasonably practicable. + +4. If You Share Adapted Material You produce, the Adapter's License You apply +must not prevent recipients of the Adapted Material from complying with this +Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to +Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, +reuse, reproduce, and Share all or a substantial portion of the contents of +the database; + +b. if You include all or a substantial portion of the database contents in +a database in which You have Sui Generis Database Rights, then the database +in which You have Sui Generis Database Rights (but not its individual contents) +is Adapted Material; and + +c. You must comply with the conditions in Section 3(a) if You Share all or +a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not replace +Your obligations under this Public License where the Licensed Rights include +other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + +a. Unless otherwise separately undertaken by the Licensor, to the extent possible, +the Licensor offers the Licensed Material as-is and as-available, and makes +no representations or warranties of any kind concerning the Licensed Material, +whether express, implied, statutory, or other. This includes, without limitation, +warranties of title, merchantability, fitness for a particular purpose, non-infringement, +absence of latent or other defects, accuracy, or the presence or absence of +errors, whether or not known or discoverable. Where disclaimers of warranties +are not allowed in full or in part, this disclaimer may not apply to You. + +b. To the extent possible, in no event will the Licensor be liable to You +on any legal theory (including, without limitation, negligence) or otherwise +for any direct, special, indirect, incidental, consequential, punitive, exemplary, +or other losses, costs, expenses, or damages arising out of this Public License +or use of the Licensed Material, even if the Licensor has been advised of +the possibility of such losses, costs, expenses, or damages. Where a limitation +of liability is not allowed in full or in part, this limitation may not apply +to You. + +c. The disclaimer of warranties and limitation of liability provided above +shall be interpreted in a manner that, to the extent possible, most closely +approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + +a. This Public License applies for the term of the Copyright and Similar Rights +licensed here. However, if You fail to comply with this Public License, then +Your rights under this Public License terminate automatically. + +b. Where Your right to use the Licensed Material has terminated under Section +6(a), it reinstates: + +1. automatically as of the date the violation is cured, provided it is cured +within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + +c. For the avoidance of doubt, this Section 6(b) does not affect any right +the Licensor may have to seek remedies for Your violations of this Public +License. + +d. For the avoidance of doubt, the Licensor may also offer the Licensed Material +under separate terms or conditions or stop distributing the Licensed Material +at any time; however, doing so will not terminate this Public License. + + e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different terms or +conditions communicated by You unless expressly agreed. + +b. Any arrangements, understandings, or agreements regarding the Licensed +Material not stated herein are separate from and independent of the terms +and conditions of this Public License. + +Section 8 – Interpretation. + +a. For the avoidance of doubt, this Public License does not, and shall not +be interpreted to, reduce, limit, restrict, or impose conditions on any use +of the Licensed Material that could lawfully be made without permission under +this Public License. + +b. To the extent possible, if any provision of this Public License is deemed +unenforceable, it shall be automatically reformed to the minimum extent necessary +to make it enforceable. If the provision cannot be reformed, it shall be severed +from this Public License without affecting the enforceability of the remaining +terms and conditions. + +c. No term or condition of this Public License will be waived and no failure +to comply consented to unless expressly agreed to by the Licensor. + +d. Nothing in this Public License constitutes or may be interpreted as a limitation +upon, or waiver of, any privileges and immunities that apply to the Licensor +or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative +Commons may elect to apply one of its public licenses to material it publishes +and in those instances will be considered the "Licensor." The text of the +Creative Commons public licenses is dedicated to the public domain under the +CC0 Public Domain Dedication. Except for the limited purpose of indicating +that material is shared under a Creative Commons public license or as otherwise +permitted by the Creative Commons policies published at creativecommons.org/policies, +Creative Commons does not authorize the use of the trademark "Creative Commons" +or any other trademark or logo of Creative Commons without its prior written +consent including, without limitation, in connection with any unauthorized +modifications to any of its public licenses or any other arrangements, understandings, +or agreements concerning use of licensed material. For the avoidance of doubt, +this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..204b93d --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,19 @@ +MIT License Copyright (c) + +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 (including the next +paragraph) 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/LICENSES/Python-2.0.txt b/LICENSES/Python-2.0.txt new file mode 100644 index 0000000..473861d --- /dev/null +++ b/LICENSES/Python-2.0.txt @@ -0,0 +1,279 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/LICENSES/Unlicense.txt b/LICENSES/Unlicense.txt new file mode 100644 index 0000000..24a8f90 --- /dev/null +++ b/LICENSES/Unlicense.txt @@ -0,0 +1,20 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute +this software, either in source code form or as a compiled binary, for any +purpose, commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and +to the detriment of our heirs and successors. We intend this dedication to +be an overt act of relinquishment in perpetuity of all present and future +rights to this software under copyright law. + +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 +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. For more information, +please refer to diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..3a9efaf --- /dev/null +++ b/README.rst @@ -0,0 +1,110 @@ +Introduction +============ + +.. image:: https://readthedocs.org/projects/adafruit-circuitpython-datetime/badge/?version=latest + :target: https://circuitpython.readthedocs.io/projects/datetime/en/latest/ + :alt: Documentation Status + +.. image:: https://img.shields.io/discord/327254708534116352.svg + :target: https://adafru.it/discord + :alt: Discord + +.. image:: https://github.com/adafruit/Adafruit_CircuitPython_datetime/workflows/Build%20CI/badge.svg + :target: https://github.com/adafruit/Adafruit_CircuitPython_datetime/actions + :alt: Build Status + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code Style: Black + +Basic date and time types. Implements a subset of the `CPython datetime module `_. + +Dependencies +============= +This driver depends on: + +* `Adafruit CircuitPython `_ + +Please ensure all dependencies are available on the CircuitPython filesystem. +This is easily achieved by downloading +`the Adafruit library and driver bundle `_. + +Installing from PyPI +===================== +On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from +PyPI `_. To install for current user: + +.. code-block:: shell + + pip3 install adafruit-circuitpython-datetime + +To install system-wide (this may be required in some cases): + +.. code-block:: shell + + sudo pip3 install adafruit-circuitpython-datetime + +To install in a virtual environment in your current project: + +.. code-block:: shell + + mkdir project-name && cd project-name + python3 -m venv .env + source .env/bin/activate + pip3 install adafruit-circuitpython-datetime + +Usage Example +============= + +.. code-block:: python + + # Example of working with a `datetime` object + # from https://docs.python.org/3/library/datetime.html#examples-of-usage-datetime + from adafruit_datetime import datetime, date, time, timezone + + # Using datetime.combine() + d = date(2005, 7, 14) + print(d) + t = time(12, 30) + print(datetime.combine(d, t)) + + # Using datetime.now() + print("Current time (GMT +1):", datetime.now()) + print("Current UTC time: ", datetime.now(timezone.utc)) + + # Using datetime.timetuple() to get tuple of all attributes + dt = datetime(2006, 11, 21, 16, 30) + tt = dt.timetuple() + for it in tt: + print(it) + + # Formatting a datetime + print( + "The {1} is {0:%d}, the {2} is {0:%B}, the {3} is {0:%I:%M%p}.".format( + dt, "day", "month", "time" + ) + ) + +Contributing +============ + +Contributions are welcome! Please read our `Code of Conduct +`_ +before contributing to help this project stay welcoming. + +Documentation +============= + +For information on building library documentation, please check out `this guide `_. + +License +======= +See LICENSE/ for details. + +Copyright (c) 2001-2021 Python Software Foundation. All rights reserved. + +Copyright (c) 2000 BeOpen.com. All rights reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. All rights reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum. All rights reserved. diff --git a/README.rst.license b/README.rst.license new file mode 100644 index 0000000..11cd75d --- /dev/null +++ b/README.rst.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + +SPDX-License-Identifier: MIT diff --git a/adafruit_datetime.py b/adafruit_datetime.py new file mode 100644 index 0000000..08798da --- /dev/null +++ b/adafruit_datetime.py @@ -0,0 +1,1723 @@ +# SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved. +# SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved. +# SPDX-FileCopyrightText: 2017 Paul Sokolovsky +# SPDX-License-Identifier: Python-2.0 + +""" +`adafruit_datetime` +================================================================================ +Concrete date/time and related types. + +See http://www.iana.org/time-zones/repository/tz-link.html for +time zone and DST data sources. + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + + +""" +# pylint: disable=too-many-lines +import time as _time +import math as _math +from micropython import const + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DateTime.git" + +# Constants +MINYEAR = const(1) +MAXYEAR = const(9999) +_MAXORDINAL = const(3652059) +_DI400Y = const(146097) +_DI100Y = const(36524) +_DI4Y = const(1461) +# https://svn.python.org/projects/sandbox/trunk/datetime/datetime.py +_DAYS_IN_MONTH = (None, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) +_DAYS_BEFORE_MONTH = (None, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334) +# Month and day names. For localized versions, see the calendar module. +_MONTHNAMES = ( + None, + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +) +_DAYNAMES = (None, "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") + +# Utility functions - universal +def _cmp(obj_x, obj_y): + return 0 if obj_x == obj_y else 1 if obj_x > obj_y else -1 + + +def _cmperror(obj_x, obj_y): + raise TypeError( + "can't compare '%s' to '%s'" % (type(obj_x).__name__, type(obj_y).__name__) + ) + + +# Utility functions - time +def _check_time_fields(hour, minute, second, microsecond, fold): + if not isinstance(hour, int): + raise TypeError("Hour expected as int") + if not 0 <= hour <= 23: + raise ValueError("hour must be in 0..23", hour) + if not 0 <= minute <= 59: + raise ValueError("minute must be in 0..59", minute) + if not 0 <= second <= 59: + raise ValueError("second must be in 0..59", second) + if not 0 <= microsecond <= 999999: + raise ValueError("microsecond must be in 0..999999", microsecond) + if fold not in (0, 1): # from CPython API + raise ValueError("fold must be either 0 or 1", fold) + + +def _check_utc_offset(name, offset): + assert name in ("utcoffset", "dst") + if offset is None: + return + if not isinstance(offset, timedelta): + raise TypeError( + "tzinfo.%s() must return None " + "or timedelta, not '%s'" % (name, type(offset)) + ) + if offset % timedelta(minutes=1) or offset.microseconds: + raise ValueError( + "tzinfo.%s() must return a whole number " + "of minutes, got %s" % (name, offset) + ) + if not -timedelta(1) < offset < timedelta(1): + raise ValueError( + "%s()=%s, must be must be strictly between" + " -timedelta(hours=24) and timedelta(hours=24)" % (name, offset) + ) + + +# pylint: disable=invalid-name +def _format_offset(off): + s = "" + if off is not None: + if off.days < 0: + sign = "-" + off = -off + else: + sign = "+" + hh, mm = divmod(off, timedelta(hours=1)) + mm, ss = divmod(mm, timedelta(minutes=1)) + s += "%s%02d:%02d" % (sign, hh, mm) + if ss or ss.microseconds: + s += ":%02d" % ss.seconds + + if ss.microseconds: + s += ".%06d" % ss.microseconds + return s + + +# pylint: disable=invalid-name, too-many-locals, too-many-nested-blocks, too-many-branches, too-many-statements +def _wrap_strftime(time_obj, strftime_fmt, timetuple): + # Don't call utcoffset() or tzname() unless actually needed. + f_replace = None # the string to use for %f + z_replace = None # the string to use for %z + Z_replace = None # the string to use for %Z + + # Scan strftime_fmt for %z and %Z escapes, replacing as needed. + newformat = [] + push = newformat.append + i, n = 0, len(strftime_fmt) + while i < n: + ch = strftime_fmt[i] + i += 1 + if ch == "%": + if i < n: + ch = strftime_fmt[i] + i += 1 + if ch == "f": + if f_replace is None: + f_replace = "%06d" % getattr(time_obj, "microsecond", 0) + newformat.append(f_replace) + elif ch == "z": + if z_replace is None: + z_replace = "" + if hasattr(time_obj, "utcoffset"): + offset = time_obj.utcoffset() + if offset is not None: + sign = "+" + if offset.days < 0: + offset = -offset + sign = "-" + h, rest = divmod(offset, timedelta(hours=1)) + m, rest = divmod(rest, timedelta(minutes=1)) + s = rest.seconds + u = offset.microseconds + if u: + z_replace = "%c%02d%02d%02d.%06d" % ( + sign, + h, + m, + s, + u, + ) + elif s: + z_replace = "%c%02d%02d%02d" % (sign, h, m, s) + else: + z_replace = "%c%02d%02d" % (sign, h, m) + assert "%" not in z_replace + newformat.append(z_replace) + elif ch == "Z": + if Z_replace is None: + Z_replace = "" + if hasattr(time_obj, "tzname"): + s = time_obj.tzname() + if s is not None: + # strftime is going to have at this: escape % + Z_replace = s.replace("%", "%%") + newformat.append(Z_replace) + else: + push("%") + push(ch) + else: + push("%") + else: + push(ch) + newformat = "".join(newformat) + return _time.strftime(newformat, timetuple) + + +# Utility functions - timezone +def _check_tzname(name): + """"Just raise TypeError if the arg isn't None or a string.""" + if name is not None and not isinstance(name, str): + raise TypeError( + "tzinfo.tzname() must return None or string, " "not '%s'" % type(name) + ) + + +def _check_tzinfo_arg(time_zone): + if time_zone is not None and not isinstance(time_zone, tzinfo): + raise TypeError("tzinfo argument must be None or of a tzinfo subclass") + + +# Utility functions - date +def _is_leap(year): + "year -> 1 if leap year, else 0." + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + + +def _days_in_month(year, month): + "year, month -> number of days in that month in that year." + assert 1 <= month <= 12, month + if month == 2 and _is_leap(year): + return 29 + return _DAYS_IN_MONTH[month] + + +def _check_date_fields(year, month, day): + if not isinstance(year, int): + raise TypeError("int expected") + if not MINYEAR <= year <= MAXYEAR: + raise ValueError("year must be in %d..%d" % (MINYEAR, MAXYEAR), year) + if not 1 <= month <= 12: + raise ValueError("month must be in 1..12", month) + dim = _days_in_month(year, month) + if not 1 <= day <= dim: + raise ValueError("day must be in 1..%d" % dim, day) + + +def _days_before_month(year, month): + "year, month -> number of days in year preceding first day of month." + assert 1 <= month <= 12, "month must be in 1..12" + return _DAYS_BEFORE_MONTH[month] + (month > 2 and _is_leap(year)) + + +def _days_before_year(year): + "year -> number of days before January 1st of year." + year = year - 1 + return year * 365 + year // 4 - year // 100 + year // 400 + + +def _ymd2ord(year, month, day): + "year, month, day -> ordinal, considering 01-Jan-0001 as day 1." + assert 1 <= month <= 12, "month must be in 1..12" + dim = _days_in_month(year, month) + assert 1 <= day <= dim, "day must be in 1..%d" % dim + return _days_before_year(year) + _days_before_month(year, month) + day + + +# pylint: disable=too-many-arguments +def _build_struct_time(tm_year, tm_month, tm_mday, tm_hour, tm_min, tm_sec, tm_isdst): + tm_wday = (_ymd2ord(tm_year, tm_month, tm_mday) + 6) % 7 + tm_yday = _days_before_month(tm_year, tm_month) + tm_mday + return _time.struct_time( + ( + tm_year, + tm_month, + tm_mday, + tm_hour, + tm_min, + tm_sec, + tm_wday, + tm_yday, + tm_isdst, + ) + ) + + +# pylint: disable=invalid-name +def _format_time(hh, mm, ss, us, timespec="auto"): + if timespec != "auto": + raise NotImplementedError("Only default timespec supported") + if us: + spec = "{:02d}:{:02d}:{:02d}.{:06d}" + else: + spec = "{:02d}:{:02d}:{:02d}" + fmt = spec + return fmt.format(hh, mm, ss, us) + + +# A 4-year cycle has an extra leap day over what we'd get from pasting +# together 4 single years. +assert _DI4Y == 4 * 365 + 1 + +# Similarly, a 400-year cycle has an extra leap day over what we'd get from +# pasting together 4 100-year cycles. +assert _DI400Y == 4 * _DI100Y + 1 + +# OTOH, a 100-year cycle has one fewer leap day than we'd get from +# pasting together 25 4-year cycles. +assert _DI100Y == 25 * _DI4Y - 1 + + +def _ord2ymd(n): + "ordinal -> (year, month, day), considering 01-Jan-0001 as day 1." + + # n is a 1-based index, starting at 1-Jan-1. The pattern of leap years + # repeats exactly every 400 years. The basic strategy is to find the + # closest 400-year boundary at or before n, then work with the offset + # from that boundary to n. Life is much clearer if we subtract 1 from + # n first -- then the values of n at 400-year boundaries are exactly + # those divisible by _DI400Y: + # + # D M Y n n-1 + # -- --- ---- ---------- ---------------- + # 31 Dec -400 -_DI400Y -_DI400Y -1 + # 1 Jan -399 -_DI400Y +1 -_DI400Y 400-year boundary + # ... + # 30 Dec 000 -1 -2 + # 31 Dec 000 0 -1 + # 1 Jan 001 1 0 400-year boundary + # 2 Jan 001 2 1 + # 3 Jan 001 3 2 + # ... + # 31 Dec 400 _DI400Y _DI400Y -1 + # 1 Jan 401 _DI400Y +1 _DI400Y 400-year boundary + n -= 1 + n400, n = divmod(n, _DI400Y) + year = n400 * 400 + 1 # ..., -399, 1, 401, ... + + # Now n is the (non-negative) offset, in days, from January 1 of year, to + # the desired date. Now compute how many 100-year cycles precede n. + # Note that it's possible for n100 to equal 4! In that case 4 full + # 100-year cycles precede the desired day, which implies the desired + # day is December 31 at the end of a 400-year cycle. + n100, n = divmod(n, _DI100Y) + + # Now compute how many 4-year cycles precede it. + n4, n = divmod(n, _DI4Y) + + # And now how many single years. Again n1 can be 4, and again meaning + # that the desired day is December 31 at the end of the 4-year cycle. + n1, n = divmod(n, 365) + + year += n100 * 100 + n4 * 4 + n1 + if n1 == 4 or n100 == 4: + assert n == 0 + return year - 1, 12, 31 + + # Now the year is correct, and n is the offset from January 1. We find + # the month via an estimate that's either exact or one too large. + leapyear = n1 == 3 and (n4 != 24 or n100 == 3) + assert leapyear == _is_leap(year) + month = (n + 50) >> 5 + preceding = _DAYS_BEFORE_MONTH[month] + (month > 2 and leapyear) + if preceding > n: # estimate is too large + month -= 1 + preceding -= _DAYS_IN_MONTH[month] + (month == 2 and leapyear) + n -= preceding + assert 0 <= n < _days_in_month(year, month) + + # Now the year and month are correct, and n is the offset from the + # start of that month: we're done! + return year, month, n + 1 + + +class timedelta: + """A timedelta object represents a duration, the difference between two dates or times.""" + + # pylint: disable=too-many-arguments + def __new__( + cls, + days=0, + seconds=0, + microseconds=0, + milliseconds=0, + minutes=0, + hours=0, + weeks=0, + ): + + # Check that all inputs are ints or floats. + if not all( + isinstance(i, (int, float)) + for i in [days, seconds, microseconds, milliseconds, minutes, hours, weeks] + ): + raise TypeError("Kwargs to this function must be int or float.") + + # Final values, all integer. + # s and us fit in 32-bit signed ints; d isn't bounded. + d = s = us = 0 + + # Normalize everything to days, seconds, microseconds. + days += weeks * 7 + seconds += minutes * 60 + hours * 3600 + microseconds += milliseconds * 1000 + + # Get rid of all fractions, and normalize s and us. + if isinstance(days, float): + dayfrac, days = _math.modf(days) + daysecondsfrac, daysecondswhole = _math.modf(dayfrac * (24.0 * 3600.0)) + assert daysecondswhole == int(daysecondswhole) # can't overflow + s = int(daysecondswhole) + assert days == int(days) + d = int(days) + else: + daysecondsfrac = 0.0 + d = days + assert isinstance(daysecondsfrac, float) + assert abs(daysecondsfrac) <= 1.0 + assert isinstance(d, int) + assert abs(s) <= 24 * 3600 + # days isn't referenced again before redefinition + + if isinstance(seconds, float): + secondsfrac, seconds = _math.modf(seconds) + assert seconds == int(seconds) + seconds = int(seconds) + secondsfrac += daysecondsfrac + assert abs(secondsfrac) <= 2.0 + else: + secondsfrac = daysecondsfrac + # daysecondsfrac isn't referenced again + assert isinstance(secondsfrac, float) + assert abs(secondsfrac) <= 2.0 + + assert isinstance(seconds, int) + days, seconds = divmod(seconds, 24 * 3600) + d += days + s += int(seconds) # can't overflow + assert isinstance(s, int) + assert abs(s) <= 2 * 24 * 3600 + # seconds isn't referenced again before redefinition + + usdouble = secondsfrac * 1e6 + assert abs(usdouble) < 2.1e6 # exact value not critical + # secondsfrac isn't referenced again + + if isinstance(microseconds, float): + microseconds = round(microseconds + usdouble) + seconds, microseconds = divmod(microseconds, 1000000) + days, seconds = divmod(seconds, 24 * 3600) + d += days + s += seconds + else: + microseconds = int(microseconds) + seconds, microseconds = divmod(microseconds, 1000000) + days, seconds = divmod(seconds, 24 * 3600) + d += days + s += seconds + microseconds = round(microseconds + usdouble) + assert isinstance(s, int) + assert isinstance(microseconds, int) + assert abs(s) <= 3 * 24 * 3600 + assert abs(microseconds) < 3.1e6 + + # Just a little bit of carrying possible for microseconds and seconds. + seconds, us = divmod(microseconds, 1000000) + s += seconds + days, s = divmod(s, 24 * 3600) + d += days + + assert isinstance(d, int) + assert isinstance(s, int) and 0 <= s < 24 * 3600 + assert isinstance(us, int) and 0 <= us < 1000000 + + if abs(d) > 999999999: + raise OverflowError("timedelta # of days is too large: %d" % d) + + self = object.__new__(cls) + self._days = d + self._seconds = s + self._microseconds = us + self._hashcode = -1 + return self + + # Instance attributes (read-only) + @property + def days(self): + """Days, Between -999999999 and 999999999 inclusive""" + return self._days + + @property + def seconds(self): + """Seconds, Between 0 and 86399 inclusive""" + return self._seconds + + @property + def microseconds(self): + """Microseconds, Between 0 and 999999 inclusive""" + return self._microseconds + + # Instance methods + def total_seconds(self): + """Return the total number of seconds contained in the duration.""" + return ( + (self._days * 86400 + self._seconds) * 10 ** 6 + self._microseconds + ) / 10 ** 6 + + def __repr__(self): + args = [] + if self._days: + args.append("days=%d" % self._days) + if self._seconds: + args.append("seconds=%d" % self._seconds) + if self._microseconds: + args.append("microseconds=%d" % self._microseconds) + if not args: + args.append("0") + return "%s.%s(%s)" % ( + self.__class__.__module__, + self.__class__.__qualname__, + ", ".join(args), + ) + + def __str__(self): + mm, ss = divmod(self._seconds, 60) + hh, mm = divmod(mm, 60) + s = "%d:%02d:%02d" % (hh, mm, ss) + if self._days: + + def plural(n): + return n, abs(n) != 1 and "s" or "" + + s = ("%d day%s, " % plural(self._days)) + s + if self._microseconds: + s = s + ".%06d" % self._microseconds + return s + + # Supported operations + def __neg__(self): + return timedelta(-self._days, -self._seconds, -self._microseconds) + + def __add__(self, other): + if isinstance(other, timedelta): + return timedelta( + self._days + other._days, + self._seconds + other._seconds, + self._microseconds + other._microseconds, + ) + return NotImplemented + + def __sub__(self, other): + if isinstance(other, timedelta): + return timedelta( + self._days - other._days, + self._seconds - other._seconds, + self._microseconds - other._microseconds, + ) + return NotImplemented + + def _to_microseconds(self): + return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds + + def __floordiv__(self, other): + if not isinstance(other, (int, timedelta)): + return NotImplemented + usec = self._to_microseconds() + if isinstance(other, timedelta): + return usec // other._to_microseconds() + return timedelta(0, 0, usec // other) + + def __mod__(self, other): + if isinstance(other, timedelta): + r = self._to_microseconds() % other._to_microseconds() + return timedelta(0, 0, r) + return NotImplemented + + def __divmod__(self, other): + if isinstance(other, timedelta): + q, r = divmod(self._to_microseconds(), other._to_microseconds()) + return q, timedelta(0, 0, r) + return NotImplemented + + def __mul__(self, other): + if isinstance(other, int): + # for CPython compatibility, we cannot use + # our __class__ here, but need a real timedelta + return timedelta( + self._days * other, self._seconds * other, self._microseconds * other + ) + if isinstance(other, float): + # a, b = other.as_integer_ratio() + # return self * a / b + usec = self._to_microseconds() + return timedelta(0, 0, round(usec * other)) + return NotImplemented + + __rmul__ = __mul__ + + # Supported comparisons + def __eq__(self, other): + if not isinstance(other, timedelta): + return False + return self._cmp(other) == 0 + + def __ne__(self, other): + if not isinstance(other, timedelta): + return True + return self._cmp(other) != 0 + + def __le__(self, other): + if not isinstance(other, timedelta): + _cmperror(self, other) + return self._cmp(other) <= 0 + + def __lt__(self, other): + if not isinstance(other, timedelta): + _cmperror(self, other) + return self._cmp(other) < 0 + + def __ge__(self, other): + if not isinstance(other, timedelta): + _cmperror(self, other) + return self._cmp(other) >= 0 + + def __gt__(self, other): + if not isinstance(other, timedelta): + _cmperror(self, other) + return self._cmp(other) > 0 + + # pylint: disable=no-self-use, protected-access + def _cmp(self, other): + assert isinstance(other, timedelta) + return _cmp(self._getstate(), other._getstate()) + + def __bool__(self): + return self._days != 0 or self._seconds != 0 or self._microseconds != 0 + + def _getstate(self): + return (self._days, self._seconds, self._microseconds) + + +# pylint: disable=no-self-use +class tzinfo: + """This is an abstract base class, meaning that this class should not + be instantiated directly. Define a subclass of tzinfo to capture information + about a particular time zone. + + """ + + def utcoffset(self, dt): + """Return offset of local time from UTC, as a timedelta + object that is positive east of UTC. + + """ + raise NotImplementedError("tzinfo subclass must override utcoffset()") + + def tzname(self, dt): + """Return the time zone name corresponding to the datetime object dt, as a string.""" + raise NotImplementedError("tzinfo subclass must override tzname()") + + # tzinfo is an abstract base class, disabling for self._offset + # pylint: disable=no-member + def fromutc(self, dt): + "datetime in UTC -> datetime in local time." + + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + dtoff = dt.utcoffset() + if dtoff is None: + raise ValueError("fromutc() requires a non-None utcoffset() " "result") + return dt + self._offset + + +class date: + """A date object represents a date (year, month and day) in an idealized calendar, + the current Gregorian calendar indefinitely extended in both directions. + Objects of this type are always naive. + + """ + + def __new__(cls, year, month, day): + """Creates a new date object. + + :param int year: Year within range, MINYEAR <= year <= MAXYEAR + :param int month: Month within range, 1 <= month <= 12 + :param int day: Day within range, 1 <= day <= number of days in the given month and year + """ + _check_date_fields(year, month, day) + self = object.__new__(cls) + self._year = year + self._month = month + self._day = day + self._hashcode = -1 + return self + + # Instance attributes (read-only) + @property + def year(self): + """Between MINYEAR and MAXYEAR inclusive.""" + return self._year + + @property + def month(self): + """Between 1 and 12 inclusive.""" + return self._month + + @property + def day(self): + """Between 1 and the number of days in the given month of the given year.""" + return self._day + + # Class Methods + @classmethod + def fromtimestamp(cls, t): + """Return the local date corresponding to the POSIX timestamp, + such as is returned by time.time(). + """ + tm_struct = _time.localtime(t) + return cls(tm_struct[0], tm_struct[1], tm_struct[2]) + + @classmethod + def fromordinal(cls, ordinal): + """Return the date corresponding to the proleptic Gregorian ordinal, + where January 1 of year 1 has ordinal 1. + + """ + if not ordinal >= 1: + raise ValueError("ordinal must be >=1") + y, m, d = _ord2ymd(ordinal) + return cls(y, m, d) + + @classmethod + def today(cls): + """Return the current local date.""" + return cls.fromtimestamp(_time.time()) + + # Instance Methods + def replace(self, year=None, month=None, day=None): + """Return a date with the same value, except for those parameters + given new values by whichever keyword arguments are specified. + If no keyword arguments are specified - values are obtained from + datetime object. + + """ + raise NotImplementedError() + + def timetuple(self): + """Return a time.struct_time such as returned by time.localtime(). + The hours, minutes and seconds are 0, and the DST flag is -1. + + """ + return _build_struct_time(self._year, self._month, self._day, 0, 0, 0, -1) + + def toordinal(self): + """Return the proleptic Gregorian ordinal of the date, where January 1 of + year 1 has ordinal 1. + """ + return _ymd2ord(self._year, self._month, self._day) + + def weekday(self): + """Return the day of the week as an integer, where Monday is 0 and Sunday is 6.""" + return (self.toordinal() + 6) % 7 + + # ISO date + def isoweekday(self): + """Return the day of the week as an integer, where Monday is 1 and Sunday is 7.""" + return self.toordinal() % 7 or 7 + + def isoformat(self): + """Return a string representing the date in ISO 8601 format, YYYY-MM-DD:""" + return "%04d-%02d-%02d" % (self._year, self._month, self._day) + + # For a date d, str(d) is equivalent to d.isoformat() + __str__ = isoformat + + def __repr__(self): + """Convert to formal string, for repr().""" + return "%s(%d, %d, %d)" % ( + "datetime." + self.__class__.__name__, + self._year, + self._month, + self._day, + ) + + # Supported comparisons + def __eq__(self, other): + if isinstance(other, date): + return self._cmp(other) == 0 + return NotImplemented + + def __le__(self, other): + if isinstance(other, date): + return self._cmp(other) <= 0 + return NotImplemented + + def __lt__(self, other): + if isinstance(other, date): + return self._cmp(other) < 0 + return NotImplemented + + def __ge__(self, other): + if isinstance(other, date): + return self._cmp(other) >= 0 + return NotImplemented + + def __gt__(self, other): + if isinstance(other, date): + return self._cmp(other) > 0 + return NotImplemented + + def _cmp(self, other): + assert isinstance(other, date) + y, m, d = self._year, self._month, self._day + y2, m2, d2 = other.year, other.month, other.day + return _cmp((y, m, d), (y2, m2, d2)) + + def __hash__(self): + if self._hashcode == -1: + self._hashcode = hash(self._getstate()) + return self._hashcode + + # Pickle support + def _getstate(self): + yhi, ylo = divmod(self._year, 256) + return (bytes([yhi, ylo, self._month, self._day]),) + + def _setstate(self, string): + yhi, ylo, self._month, self._day = string + self._year = yhi * 256 + ylo + + +class timezone(tzinfo): + """The timezone class is a subclass of tzinfo, each instance of which represents a + timezone defined by a fixed offset from UTC. + + Objects of this class cannot be used to represent timezone information in the locations + where different offsets are used in different days of the year or where historical changes + have been made to civil time. + + """ + + # Sentinel value to disallow None + _Omitted = object() + + def __new__(cls, offset, name=_Omitted): + if not isinstance(offset, timedelta): + raise TypeError("offset must be a timedelta") + if name is cls._Omitted: + if not offset: + return cls.utc + name = None + elif not isinstance(name, str): + raise TypeError("name must be a string") + if not cls.minoffset <= offset <= cls.maxoffset: + raise ValueError( + "offset must be a timedelta" + " strictly between -timedelta(hours=24) and" + " timedelta(hours=24)." + ) + if offset.microseconds != 0 or offset.seconds % 60 != 0: + raise ValueError( + "offset must be a timedelta" " representing a whole number of minutes" + ) + return cls._create(offset, name) + + # pylint: disable=protected-access + @classmethod + def _create(cls, offset, name=None): + """High-level creation for a timezone object.""" + self = tzinfo.__new__(cls) + self._offset = offset + self._name = name + return self + + # Instance methods + def utcoffset(self, dt): + if isinstance(dt, datetime) or dt is None: + return self._offset + raise TypeError("utcoffset() argument must be a datetime instance" " or None") + + def tzname(self, dt): + if isinstance(dt, datetime) or dt is None: + if self._name is None: + return self._name_from_offset(self._offset) + return self._name + raise TypeError("tzname() argument must be a datetime instance" " or None") + + # Comparison to other timezone objects + def __eq__(self, other): + if not isinstance(other, timezone): + return False + return self._offset == other._offset + + def __hash__(self): + return hash(self._offset) + + def __repr__(self): + """Convert to formal string, for repr().""" + if self is self.utc: + return "datetime.timezone.utc" + if self._name is None: + return "%s(%r)" % ("datetime." + self.__class__.__name__, self._offset) + return "%s(%r, %r)" % ( + "datetime." + self.__class__.__name__, + self._offset, + self._name, + ) + + def __str__(self): + return self.tzname(None) + + @staticmethod + def _name_from_offset(delta): + if delta < timedelta(0): + sign = "-" + delta = -delta + else: + sign = "+" + hours, rest = divmod(delta, timedelta(hours=1)) + minutes = rest // timedelta(minutes=1) + return "UTC{}{:02d}:{:02d}".format(sign, hours, minutes) + + maxoffset = timedelta(hours=23, minutes=59) + minoffset = -maxoffset + + +class time: + """A time object represents a (local) time of day, independent of + any particular day, and subject to adjustment via a tzinfo object. + + """ + + # pylint: disable=redefined-outer-name + def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0): + _check_time_fields(hour, minute, second, microsecond, fold) + _check_tzinfo_arg(tzinfo) + self = object.__new__(cls) + self._hour = hour + self._minute = minute + self._second = second + self._microsecond = microsecond + self._tzinfo = tzinfo + self._fold = fold + self._hashcode = -1 + return self + + # Instance attributes (read-only) + @property + def hour(self): + """In range(24).""" + return self._hour + + @property + def minute(self): + """In range(60).""" + return self._minute + + @property + def second(self): + """In range(60).""" + return self._second + + @property + def microsecond(self): + """In range(1000000).""" + return self._microsecond + + @property + def fold(self): + """Fold.""" + return self._fold + + @property + def tzinfo(self): + """The object passed as the tzinfo argument to + the time constructor, or None if none was passed. + """ + return self._tzinfo + + # Instance methods + def isoformat(self, timespec="auto"): + """Return a string representing the time in ISO 8601 format, one of: + HH:MM:SS.ffffff, if microsecond is not 0 + + HH:MM:SS, if microsecond is 0 + + HH:MM:SS.ffffff+HH:MM[:SS[.ffffff]], if utcoffset() does not return None + + HH:MM:SS+HH:MM[:SS[.ffffff]], if microsecond is 0 and utcoffset() does not return None + + """ + s = _format_time( + self._hour, self._minute, self._second, self._microsecond, timespec + ) + tz = self._tzstr() + if tz: + s += tz + return s + + # For a time t, str(t) is equivalent to t.isoformat() + __str__ = isoformat + + def strftime(self, fmt): + """Format using strftime(). The date part of the timestamp passed + to underlying strftime should not be used. + """ + # The year must be >= 1000 else Python's strftime implementation + # can raise a bogus exception. + timetuple = (1900, 1, 1, self._hour, self._minute, self._second, 0, 1, -1) + return _wrap_strftime(self, fmt, timetuple) + + def utcoffset(self): + """Return the timezone offset in minutes east of UTC (negative west of + UTC).""" + if self._tzinfo is None: + return None + offset = self._tzinfo.utcoffset(None) + _check_utc_offset("utcoffset", offset) + return offset + + def tzname(self): + """Return the timezone name. + + Note that the name is 100% informational -- there's no requirement that + it mean anything in particular. For example, "GMT", "UTC", "-500", + "-5:00", "EDT", "US/Eastern", "America/New York" are all valid replies. + """ + if self._tzinfo is None: + return None + name = self._tzinfo.tzname(None) + _check_tzname(name) + return name + + # Standard conversions and comparisons + def __eq__(self, other): + if not isinstance(other, time): + return NotImplemented + return self._cmp(other, allow_mixed=True) == 0 + + def __le__(self, other): + if not isinstance(other, time): + return NotImplemented + return self._cmp(other) <= 0 + + def __lt__(self, other): + if not isinstance(other, time): + return NotImplemented + return self._cmp(other) < 0 + + def __ge__(self, other): + if not isinstance(other, time): + return NotImplemented + return self._cmp(other) >= 0 + + def __gt__(self, other): + if not isinstance(other, time): + return NotImplemented + return self._cmp(other) > 0 + + def _cmp(self, other, allow_mixed=False): + assert isinstance(other, time) + mytz = self._tzinfo + ottz = other.tzinfo + myoff = otoff = None + + if mytz is ottz: + base_compare = True + else: + myoff = self.utcoffset() + otoff = other.utcoffset() + base_compare = myoff == otoff + + if base_compare: + return _cmp( + (self._hour, self._minute, self._second, self._microsecond), + (other.hour, other.minute, other.second, other.microsecond), + ) + if myoff is None or otoff is None: + if not allow_mixed: + raise TypeError("cannot compare naive and aware times") + return 2 # arbitrary non-zero value + myhhmm = self._hour * 60 + self._minute - myoff // timedelta(minutes=1) + othhmm = other.hour * 60 + other.minute - otoff // timedelta(minutes=1) + return _cmp( + (myhhmm, self._second, self._microsecond), + (othhmm, other.second, other.microsecond), + ) + + def __hash__(self): + """Hash.""" + if self._hashcode == -1: + t = self + tzoff = t.utcoffset() + if not tzoff: # zero or None + self._hashcode = hash(t._getstate()[0]) + else: + h, m = divmod( + timedelta(hours=self.hour, minutes=self.minute) - tzoff, + timedelta(hours=1), + ) + assert not m % timedelta(minutes=1), "whole minute" + m //= timedelta(minutes=1) + if 0 <= h < 24: + self._hashcode = hash(time(h, m, self.second, self.microsecond)) + else: + self._hashcode = hash((h, m, self.second, self.microsecond)) + return self._hashcode + + def _tzstr(self, sep=":"): + """Return formatted timezone offset (+xx:xx) or None.""" + off = self.utcoffset() + if off is not None: + if off.days < 0: + sign = "-" + off = -1 * off + else: + sign = "+" + hh, mm = divmod(off, timedelta(hours=1)) + assert not mm % timedelta(minutes=1), "whole minute" + mm //= timedelta(minutes=1) + assert 0 <= hh < 24 + off = "%s%02d%s%02d" % (sign, hh, sep, mm) + return off + + def __format__(self, fmt): + if not isinstance(fmt, str): + raise TypeError("must be str, not %s" % type(fmt).__name__) + if len(fmt) != 0: + return self.strftime(fmt) + return str(self) + + def __repr__(self): + """Convert to formal string, for repr().""" + if self._microsecond != 0: + s = ", %d, %d" % (self._second, self._microsecond) + elif self._second != 0: + s = ", %d" % self._second + else: + s = "" + s = "%s(%d, %d%s)" % ( + "datetime." + self.__class__.__name__, + self._hour, + self._minute, + s, + ) + if self._tzinfo is not None: + assert s[-1:] == ")" + s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")" + return s + + # Pickle support + def _getstate(self, protocol=3): + us2, us3 = divmod(self._microsecond, 256) + us1, us2 = divmod(us2, 256) + h = self._hour + if self._fold and protocol > 3: + h += 128 + basestate = bytes([h, self._minute, self._second, us1, us2, us3]) + if not self._tzinfo is None: + return (basestate, self._tzinfo) + return (basestate,) + + +# pylint: disable=too-many-instance-attributes, too-many-public-methods +class datetime(date): + """A datetime object is a single object containing all the information + from a date object and a time object. Like a date object, datetime assumes + the current Gregorian calendar extended in both directions; like a time object, + datetime assumes there are exactly 3600*24 seconds in every day. + + """ + + # pylint: disable=redefined-outer-name + def __new__( + cls, + year, + month, + day, + hour=0, + minute=0, + second=0, + microsecond=0, + tzinfo=None, + *, + fold=0 + ): + _check_date_fields(year, month, day) + _check_time_fields(hour, minute, second, microsecond, fold) + _check_tzinfo_arg(tzinfo) + + self = object.__new__(cls) + self._year = year + self._month = month + self._day = day + self._hour = hour + self._minute = minute + self._second = second + self._microsecond = microsecond + self._tzinfo = tzinfo + self._fold = fold + self._hashcode = -1 + return self + + # Read-only instance attributes + @property + def year(self): + """Between MINYEAR and MAXYEAR inclusive.""" + return self._year + + @property + def month(self): + """Between 1 and 12 inclusive.""" + return self._month + + @property + def day(self): + """Between 1 and the number of days in the given month of the given year.""" + return self._day + + @property + def hour(self): + """In range(24).""" + return self._hour + + @property + def minute(self): + """In range (60)""" + return self._minute + + @property + def second(self): + """In range (60)""" + return self._second + + @property + def microsecond(self): + """In range (1000000)""" + return self._microsecond + + @property + def tzinfo(self): + """The object passed as the tzinfo argument to the datetime constructor, + or None if none was passed. + """ + return self._tzinfo + + # Class methods + + # pylint: disable=protected-access + @classmethod + def _fromtimestamp(cls, t, utc, tz): + """Construct a datetime from a POSIX timestamp (like time.time()). + A timezone info object may be passed in as well. + """ + frac, t = _math.modf(t) + us = round(frac * 1e6) + if us >= 1000000: + t += 1 + us -= 1000000 + elif us < 0: + t -= 1 + us += 1000000 + + converter = _time.gmtime if utc else _time.localtime + struct_time = converter(t) + ss = min(struct_time[5], 59) # clamp out leap seconds if the platform has them + result = cls( + struct_time[0], + struct_time[1], + struct_time[2], + struct_time[3], + struct_time[4], + ss, + us, + tz, + ) + if tz is None: + # As of version 2015f max fold in IANA database is + # 23 hours at 1969-09-30 13:00:00 in Kwajalein. + # Let's probe 24 hours in the past to detect a transition: + max_fold_seconds = 24 * 3600 + + struct_time = converter(t - max_fold_seconds)[:6] + probe1 = cls( + struct_time[0], + struct_time[1], + struct_time[2], + struct_time[3], + struct_time[4], + struct_time[5], + us, + tz, + ) + trans = result - probe1 - timedelta(0, max_fold_seconds) + if trans.days < 0: + struct_time = converter(t + trans // timedelta(0, 1))[:6] + probe2 = cls( + struct_time[0], + struct_time[1], + struct_time[2], + struct_time[3], + struct_time[4], + struct_time[5], + us, + tz, + ) + if probe2 == result: + result._fold = 1 + else: + result = tz.fromutc(result) + return result + + ## pylint: disable=arguments-differ + @classmethod + def fromtimestamp(cls, timestamp, tz=None): + return cls._fromtimestamp(timestamp, tz is not None, tz) + + @classmethod + def now(cls, timezone=None): + """Return the current local date and time.""" + return cls.fromtimestamp(_time.time(), timezone) + + @classmethod + def utcfromtimestamp(cls, timestamp): + """Return the UTC datetime corresponding to the POSIX timestamp, with tzinfo None""" + return cls._fromtimestamp(timestamp, True, None) + + @classmethod + def combine(cls, date, time, tzinfo=True): + """Return a new datetime object whose date components are equal to the + given date object’s, and whose time components are equal to the given time object’s. + + """ + if not isinstance(date, _date_class): + raise TypeError("date argument must be a date instance") + if not isinstance(time, _time_class): + raise TypeError("time argument must be a time instance") + if tzinfo is True: + tzinfo = time.tzinfo + return cls( + date.year, + date.month, + date.day, + time.hour, + time.minute, + time.second, + time.microsecond, + tzinfo, + fold=time.fold, + ) + + # Instance methods + def _mktime(self): + """Return integer POSIX timestamp.""" + epoch = datetime(1970, 1, 1) + max_fold_seconds = 24 * 3600 + t = (self - epoch) // timedelta(0, 1) + + def local(u): + y, m, d, hh, mm, ss = _time.localtime(u)[:6] + return (datetime(y, m, d, hh, mm, ss) - epoch) // timedelta(0, 1) + + # Our goal is to solve t = local(u) for u. + a = local(t) - t + u1 = t - a + t1 = local(u1) + if t1 == t: + # We found one solution, but it may not be the one we need. + # Look for an earlier solution (if `fold` is 0), or a + # later one (if `fold` is 1). + u2 = u1 + (-max_fold_seconds, max_fold_seconds)[self._fold] + b = local(u2) - u2 + if a == b: + return u1 + else: + b = t1 - u1 + assert a != b + u2 = t - b + t2 = local(u2) + if t2 == t: + return u2 + if t1 == t: + return u1 + # We have found both offsets a and b, but neither t - a nor t - b is + # a solution. This means t is in the gap. + return (max, min)[self._fold](u1, u2) + + def date(self): + """Return date object with same year, month and day.""" + return _date_class(self._year, self._month, self._day) + + def time(self): + """Return time object with same hour, minute, second, microsecond and fold. + tzinfo is None. See also method timetz(). + + """ + return _time_class( + self._hour, self._minute, self._second, self._microsecond, fold=self._fold + ) + + def dst(self): + """If tzinfo is None, returns None, else returns self.tzinfo.dst(self), + and raises an exception if the latter doesn’t return None or a timedelta + object with magnitude less than one day. + + """ + if self._tzinfo is None: + return None + offset = self._tzinfo.dst(self) + _check_utc_offset("dst", offset) + return offset + + def timetuple(self): + """Return local time tuple compatible with time.localtime().""" + dst = self.dst() + if dst is None: + dst = -1 + elif dst: + dst = 1 + else: + dst = 0 + return _build_struct_time( + self.year, self.month, self.day, self.hour, self.minute, self.second, dst + ) + + def utcoffset(self): + """If tzinfo is None, returns None, else returns + self.tzinfo.utcoffset(self), and raises an exception + if the latter doesn’t return None or a timedelta object + with magnitude less than one day. + + """ + if self._tzinfo is None: + return None + offset = self._tzinfo.utcoffset(self) + _check_utc_offset("utcoffset", offset) + return offset + + def toordinal(self): + """Return the proleptic Gregorian ordinal of the date.""" + return _ymd2ord(self._year, self._month, self._day) + + def timestamp(self): + "Return POSIX timestamp as float" + if not self._tzinfo is None: + return (self - _EPOCH).total_seconds() + s = self._mktime() + return s + self.microsecond / 1e6 + + def weekday(self): + """Return the day of the week as an integer, where Monday is 0 and Sunday is 6.""" + return (self.toordinal() + 6) % 7 + + def strftime(self, fmt): + """Format using strftime(). The date part of the timestamp passed + to underlying strftime should not be used. + """ + # The year must be >= 1000 else Python's strftime implementation + # can raise a bogus exception. + timetuple = (1900, 1, 1, self._hour, self._minute, self._second, 0, 1, -1) + return _wrap_strftime(self, fmt, timetuple) + + def __format__(self, fmt): + if len(fmt) != 0: + return self.strftime(fmt) + return str(self) + + def __repr__(self): + """Convert to formal string, for repr().""" + L = [ + self._year, + self._month, + self._day, # These are never zero + self._hour, + self._minute, + self._second, + self._microsecond, + ] + if L[-1] == 0: + del L[-1] + if L[-1] == 0: + del L[-1] + s = ", ".join(map(str, L)) + s = "%s(%s)" % ("datetime." + self.__class__.__name__, s) + if self._tzinfo is not None: + assert s[-1:] == ")" + s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")" + return s + + def isoformat(self, sep="T", timespec="auto"): + """Return a string representing the date and time in + ISO8601 format. + + """ + s = "%04d-%02d-%02d%c" % ( + self._year, + self._month, + self._day, + sep, + ) + _format_time( + self._hour, self._minute, self._second, self._microsecond, timespec + ) + + off = self.utcoffset() + tz = _format_offset(off) + if tz: + s += tz + + return s + + def __str__(self): + "Convert to string, for str()." + return self.isoformat(sep=" ") + + def replace( + self, + year=None, + month=None, + day=None, + hour=None, + minute=None, + second=None, + microsecond=None, + tzinfo=True, + *, + fold=None + ): + """Return a datetime with the same attributes, + except for those attributes given new values by + whichever keyword arguments are specified. + + """ + if year is None: + year = self.year + if month is None: + month = self.month + if day is None: + day = self.day + if hour is None: + hour = self.hour + if minute is None: + minute = self.minute + if second is None: + second = self.second + if microsecond is None: + microsecond = self.microsecond + if tzinfo is True: + tzinfo = self.tzinfo + if fold is None: + fold = self._fold + return type(self)( + year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold + ) + + # Comparisons of datetime objects. + def __eq__(self, other): + if not isinstance(other, datetime): + return False + return self._cmp(other, allow_mixed=True) == 0 + + def __le__(self, other): + if not isinstance(other, datetime): + _cmperror(self, other) + return self._cmp(other) <= 0 + + def __lt__(self, other): + if not isinstance(other, datetime): + _cmperror(self, other) + return self._cmp(other) < 0 + + def __ge__(self, other): + if not isinstance(other, datetime): + _cmperror(self, other) + return self._cmp(other) >= 0 + + def __gt__(self, other): + if not isinstance(other, datetime): + _cmperror(self, other) + return self._cmp(other) > 0 + + def _cmp(self, other, allow_mixed=False): + assert isinstance(other, datetime) + mytz = self._tzinfo + ottz = other.tzinfo + myoff = otoff = None + + if mytz is ottz: + base_compare = True + else: + myoff = self.utcoffset() + otoff = other.utcoffset() + # Assume that allow_mixed means that we are called from __eq__ + if allow_mixed: + if myoff != self.replace(fold=not self._fold).utcoffset(): + return 2 + if otoff != other.replace(fold=not other.fold).utcoffset(): + return 2 + base_compare = myoff == otoff + + if base_compare: + return _cmp( + ( + self._year, + self._month, + self._day, + self._hour, + self._minute, + self._second, + self._microsecond, + ), + ( + other.year, + other.month, + other.day, + other.hour, + other.minute, + other.second, + other.microsecond, + ), + ) + if myoff is None or otoff is None: + if not allow_mixed: + raise TypeError("cannot compare naive and aware datetimes") + return 2 # arbitrary non-zero value + diff = self - other # this will take offsets into account + if diff.days < 0: + return -1 + return 1 if diff else 0 + + def __add__(self, other): + "Add a datetime and a timedelta." + if not isinstance(other, timedelta): + return NotImplemented + delta = timedelta( + self.toordinal(), + hours=self._hour, + minutes=self._minute, + seconds=self._second, + microseconds=self._microsecond, + ) + delta += other + hour, rem = divmod(delta._seconds, 3600) + minute, second = divmod(rem, 60) + if 0 < delta._days <= _MAXORDINAL: + return type(self).combine( + date.fromordinal(delta._days), + time(hour, minute, second, delta._microseconds, tzinfo=self._tzinfo), + ) + raise OverflowError("result out of range") + + __radd__ = __add__ + + def __sub__(self, other): + "Subtract two datetimes, or a datetime and a timedelta." + if not isinstance(other, datetime): + if isinstance(other, timedelta): + return self + -other + return NotImplemented + + days1 = self.toordinal() + days2 = other.toordinal() + secs1 = self._second + self._minute * 60 + self._hour * 3600 + secs2 = other._second + other._minute * 60 + other._hour * 3600 + base = timedelta( + days1 - days2, secs1 - secs2, self._microsecond - other._microsecond + ) + if self._tzinfo is other._tzinfo: + return base + myoff = self.utcoffset() + otoff = other.utcoffset() + if myoff == otoff: + return base + if myoff is None or otoff is None: + raise TypeError("cannot mix naive and timezone-aware time") + return base + otoff - myoff + + def __hash__(self): + if self._hashcode == -1: + t = self + tzoff = t.utcoffset() + if tzoff is None: + self._hashcode = hash(t._getstate()[0]) + else: + days = _ymd2ord(self.year, self.month, self.day) + seconds = self.hour * 3600 + self.minute * 60 + self.second + self._hashcode = hash( + timedelta(days, seconds, self.microsecond) - tzoff + ) + return self._hashcode + + def _getstate(self): + protocol = 3 + yhi, ylo = divmod(self._year, 256) + us2, us3 = divmod(self._microsecond, 256) + us1, us2 = divmod(us2, 256) + m = self._month + if self._fold and protocol > 3: + m += 128 + basestate = bytes( + [ + yhi, + ylo, + m, + self._day, + self._hour, + self._minute, + self._second, + us1, + us2, + us3, + ] + ) + if not self._tzinfo is None: + return (basestate, self._tzinfo) + return (basestate,) + + +# Module exports +# pylint: disable=protected-access +timezone.utc = timezone._create(timedelta(0)) +timezone.min = timezone._create(timezone.minoffset) +timezone.max = timezone._create(timezone.maxoffset) +_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) +_date_class = date +_time_class = time diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5aca98376a1f7e593ebd9cf41a808512c2135635 GIT binary patch literal 4414 zcmd^BX;4#F6n=SG-XmlONeGrD5E6J{RVh+e928U#MG!$jWvO+UsvWh`x&VqGNx*en zx=qox7Dqv{kPwo%fZC$dDwVpRtz{HzTkSs8QhG0)%Y=-3@Kt!4ag|JcIo?$-F|?bXVS9UDUyev>MVZQ(H8K4#;BQW-t2CPorj8^KJrMX}QK zp+e<;4ldpXz~=)2GxNy811&)gt-}Q*yVQpsxr@VMoA##{)$1~=bZ1MmjeFw?uT(`8 z^g=09<=zW%r%buwN%iHtuKSg|+r7HkT0PYN*_u9k1;^Ss-Z!RBfJ?Un4w(awqp2b3 z%+myoFis_lTlCrGx2z$0BQdh+7?!JK#9K9@Z!VrG zNj6gK5r(b4?YDOLw|DPRoN7bdP{(>GEG41YcN~4r_SUHU2hgVtUwZG@s%edC;k7Sn zC)RvEnlq~raE2mY2ko64^m1KQL}3riixh?#J{o)IT+K-RdHae2eRX91-+g!y`8^># z-zI0ir>P%Xon)!@xp-BK2bDYUB9k613NRrY6%lVjbFcQc*pRqiK~8xtkNPLxt}e?&QsTB}^!39t_%Qb)~Ukn0O%iC;zt z<&A-y;3h++)>c1br`5VFM~5(83!HKx$L+my8sW_c#@x*|*vB1yU)_dt3vH;2hqPWx zAl^6@?ipx&U7pf`a*>Yq6C85nb+B=Fnn+(id$W#WB^uHAcZVG`qg;rWB}ubvi(Y>D z$ei>REw$#xp0SHAd^|1hq&9HJ=jKK8^zTH~nk)G?yUcmTh9vUM6Y0LMw4(gYVY$D$ zGl&WY&H<)BbJ&3sYbKjx1j^=3-0Q#f^}(aP1?8^`&FUWMp|rmtpK)bLQ1Zo?^s4jqK=Lfg*9&geMGVQ z#^-*!V`fG@;H&{M9S8%+;|h&Qrxym0Ar>WT4BCVLR8cGXF=JmEYN(sNT(9vl+S|%g z8r7nXQ(95i^`=+XHo|){$vf2$?=`F$^&wFlYXyXg$B{a>$-Fp+V}+D;9k=~Xl~?C4 zAB-;RKXdUzBJE{V&d&%R>aEfFe;vxqI$0@hwVM}gFeQR@j}a>DDxR+n+-*6|_)k%% z*mSpDV|=5I9!&VC&9tD%fcVygWZV!iIo2qFtm#!*(s|@ZT33*Ad;+<|3^+yrp*;oH zBSYLV(H1zTU?2WjrCQoQW)Z>J2a=dTriuvezBmu16`tM2fm7Q@d4^iqII-xFpwHGI zn9CL}QE*1vdj2PX{PIuqOe5dracsciH6OlAZATvE8rj6ykqdIjal2 z0S0S~PwHb-5?OQ-tU-^KTG@XNrEVSvo|HIP?H;7ZhYeZkhSqh-{reE!5di;1zk$#Y zCe7rOnlzFYJ6Z#Hm$GoidKB=2HBCwm`BbZVeZY4ukmG%1uz7p2URs6c9j-Gjj^oQV zsdDb3@k2e`C$1I5ML5U0Qs0C1GAp^?!*`=|Nm(vWz3j*j*8ucum2;r0^-6Aca=Gv) zc%}&;!+_*S2tlnnJnz0EKeRmw-Y!@9ob!XQBwiv}^u9MkaXHvM=!<3YX;+2#5Cj5pp?FEK750S3BgeSDtaE^ zXUM@xoV6yBFKfzvY20V&Lr0yC + CircuitPython Reference Documentation + CircuitPython Support Forum + Discord Chat + Adafruit Learning System + Adafruit Blog + Adafruit Store + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/index.rst.license b/docs/index.rst.license new file mode 100644 index 0000000..11cd75d --- /dev/null +++ b/docs/index.rst.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + +SPDX-License-Identifier: MIT diff --git a/examples/datetime_simpletest.py b/examples/datetime_simpletest.py new file mode 100644 index 0000000..5d1fdc3 --- /dev/null +++ b/examples/datetime_simpletest.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved. +# SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved. +# SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries +# SPDX-License-Identifier: Python-2.0 + +# Example of working with a `datetime` object +# from https://docs.python.org/3/library/datetime.html#examples-of-usage-datetime +from adafruit_datetime import datetime, date, time, timezone + +# Using datetime.combine() +d = date(2005, 7, 14) +print(d) +t = time(12, 30) +print(datetime.combine(d, t)) + +# Using datetime.now() +print("Current time (GMT +1):", datetime.now()) +print("Current UTC time: ", datetime.now(timezone.utc)) + +# Using datetime.timetuple() to get tuple of all attributes +dt = datetime(2006, 11, 21, 16, 30) +tt = dt.timetuple() +for it in tt: + print(it) + +# Formatting a datetime +print( + "The {1} is {0:%d}, the {2} is {0:%B}, the {3} is {0:%I:%M%p}.".format( + dt, "day", "month", "time" + ) +) diff --git a/examples/datetime_time.py b/examples/datetime_time.py new file mode 100644 index 0000000..588eb2d --- /dev/null +++ b/examples/datetime_time.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved. +# SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved. +# SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries +# SPDX-License-Identifier: Python-2.0 + +# Example of working with a `time` object +# from https://docs.python.org/3/library/datetime.html#examples-of-usage-time +from adafruit_datetime import time, timezone + +# Create a new time object +t = time(12, 10, 30, tzinfo=timezone.utc) + +# ISO 8601 formatted string +iso_time = t.isoformat() +print("ISO8601-Formatted Time:", iso_time) + +# Timezone name +print("Timezone Name:", t.tzname()) + +# Return a string representing the time, controlled by an explicit format string +strf_time = t.strftime("%H:%M:%S %Z") +print("Formatted time string:", strf_time) + +# Specifies a format string in formatted string literals +print("The time is {:%H:%M}.".format(t)) diff --git a/examples/datetime_timedelta.py b/examples/datetime_timedelta.py new file mode 100644 index 0000000..7ecf5f7 --- /dev/null +++ b/examples/datetime_timedelta.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved. +# SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved. +# SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries +# SPDX-License-Identifier: Python-2.0 + +# Example of working with a `timedelta` object +# from https://docs.python.org/3/library/datetime.html#examples-of-usage-timedelta +from adafruit_datetime import timedelta + +# Example of normalization +year = timedelta(days=365) +another_year = timedelta(weeks=40, days=84, hours=23, minutes=50, seconds=600) +print("Total seconds in the year: ", year.total_seconds()) + +# Example of timedelta arithmetic +year = timedelta(days=365) +ten_years = 10 * year +print("Days in ten years:", ten_years) + +nine_years = ten_years - year +print("Days in nine years:", nine_years) + +three_years = nine_years // 3 +print("Days in three years:", three_years, three_years.days // 365) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f3c35ae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò +# +# SPDX-License-Identifier: Unlicense + +[tool.black] +target-version = ['py35'] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c50f8d3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2021 Brent Rubell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +Adafruit-Blinka diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fe0d5bc --- /dev/null +++ b/setup.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2021 Brent Rubell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +"""A setuptools based setup module. + +See: +https://packaging.python.org/en/latest/distributing.html +https://github.com/pypa/sampleproject +""" + +from setuptools import setup, find_packages + +# To use a consistent encoding +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, "README.rst"), encoding="utf-8") as f: + long_description = f.read() + +setup( + name="adafruit-circuitpython-datetime", + use_scm_version=True, + setup_requires=["setuptools_scm"], + description="Subset of CPython datetime module", + long_description=long_description, + long_description_content_type="text/x-rst", + # The project's main homepage. + url="https://github.com/adafruit/Adafruit_CircuitPython_datetime", + # Author details + author="Adafruit Industries", + author_email="circuitpython@adafruit.com", + install_requires=[ + "Adafruit-Blinka", + ], + # Choose your license + license="MIT", + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "Topic :: System :: Hardware", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + ], + # What does your project relate to? + keywords="adafruit blinka circuitpython micropython datetime date time timedelta tzinfo " + "timezone", + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + # TODO: IF LIBRARY FILES ARE A PACKAGE FOLDER, + # CHANGE `py_modules=['...']` TO `packages=['...']` + py_modules=["adafruit_datetime"], +) diff --git a/tests/test_date.py b/tests/test_date.py new file mode 100644 index 0000000..8b9626c --- /dev/null +++ b/tests/test_date.py @@ -0,0 +1,458 @@ +# SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved. +# SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved. +# SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries +# SPDX-License-Identifier: Python-2.0 +# Implements a subset of https://github.com/python/cpython/blob/master/Lib/test/datetimetester.py +import sys +import unittest + +# CPython standard implementation +from datetime import date as cpython_date +from datetime import MINYEAR, MAXYEAR + +# CircuitPython subset implementation +sys.path.append("..") +from adafruit_datetime import date as cpy_date + +# An arbitrary collection of objects of non-datetime types, for testing +# mixed-type comparisons. +OTHERSTUFF = (10, 34.5, "abc", {}, [], ()) + + +class TestDate(unittest.TestCase): + def test_basic_attributes(self): + dt = cpy_date(2002, 3, 1) + dt_2 = cpython_date(2002, 3, 1) + self.assertEqual(dt.year, dt_2.year) + self.assertEqual(dt.month, dt_2.month) + self.assertEqual(dt.day, dt_2.day) + + def test_bad_constructor_arguments(self): + # bad years + cpy_date(MINYEAR, 1, 1) # no exception + cpy_date(MAXYEAR, 1, 1) # no exception + self.assertRaises(ValueError, cpy_date, MINYEAR - 1, 1, 1) + self.assertRaises(ValueError, cpy_date, MAXYEAR + 1, 1, 1) + # bad months + cpy_date(2000, 1, 1) # no exception + cpy_date(2000, 12, 1) # no exception + self.assertRaises(ValueError, cpy_date, 2000, 0, 1) + self.assertRaises(ValueError, cpy_date, 2000, 13, 1) + # bad days + cpy_date(2000, 2, 29) # no exception + cpy_date(2004, 2, 29) # no exception + cpy_date(2400, 2, 29) # no exception + self.assertRaises(ValueError, cpy_date, 2000, 2, 30) + self.assertRaises(ValueError, cpy_date, 2001, 2, 29) + self.assertRaises(ValueError, cpy_date, 2100, 2, 29) + self.assertRaises(ValueError, cpy_date, 1900, 2, 29) + self.assertRaises(ValueError, cpy_date, 2000, 1, 0) + self.assertRaises(ValueError, cpy_date, 2000, 1, 32) + + def test_hash_equality(self): + d = cpy_date(2000, 12, 31) + e = cpy_date(2000, 12, 31) + self.assertEqual(d, e) + self.assertEqual(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + self.assertEqual(len(dic), 1) + self.assertEqual(dic[d], 2) + self.assertEqual(dic[e], 2) + + d = cpy_date(2001, 1, 1) + e = cpy_date(2001, 1, 1) + self.assertEqual(d, e) + self.assertEqual(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + self.assertEqual(len(dic), 1) + self.assertEqual(dic[d], 2) + self.assertEqual(dic[e], 2) + + def test_fromtimestamp(self): + import time + + # Try an arbitrary fixed value. + year, month, day = 1999, 9, 19 + ts = time.mktime((year, month, day, 0, 0, 0, 0, 0, -1)) + d = cpy_date.fromtimestamp(ts) + self.assertEqual(d.year, year) + self.assertEqual(d.month, month) + self.assertEqual(d.day, day) + + # TODO: Test this when timedelta is added in + @unittest.skip("Skip for CircuitPython - timedelta() not yet implemented.") + def test_today(self): + import time + + # We claim that today() is like fromtimestamp(time.time()), so + # prove it. + for dummy in range(3): + today = cpy_date.today() + ts = time.time() + todayagain = cpy_date.fromtimestamp(ts) + if today == todayagain: + break + # There are several legit reasons that could fail: + # 1. It recently became midnight, between the today() and the + # time() calls. + # 2. The platform time() has such fine resolution that we'll + # never get the same value twice. + # 3. The platform time() has poor resolution, and we just + # happened to call today() right before a resolution quantum + # boundary. + # 4. The system clock got fiddled between calls. + # In any case, wait a little while and try again. + time.sleep(0.1) + + # It worked or it didn't. If it didn't, assume it's reason #2, and + # let the test pass if they're within half a second of each other. + self.assertTrue( + today == todayagain or abs(todayagain - today) < timedelta(seconds=0.5) + ) + + def test_weekday(self): + for i in range(7): + # March 4, 2002 is a Monday + self.assertEqual( + cpy_date(2002, 3, 4 + i).weekday(), + cpython_date(2002, 3, 4 + i).weekday(), + ) + self.assertEqual( + cpy_date(2002, 3, 4 + i).isoweekday(), + cpython_date(2002, 3, 4 + i).isoweekday(), + ) + # January 2, 1956 is a Monday + self.assertEqual( + cpy_date(1956, 1, 2 + i).weekday(), + cpython_date(1956, 1, 2 + i).weekday(), + ) + self.assertEqual( + cpy_date(1956, 1, 2 + i).isoweekday(), + cpython_date(1956, 1, 2 + i).isoweekday(), + ) + + @unittest.skip( + "Skip for CircuitPython - isocalendar() not implemented for date objects." + ) + def test_isocalendar(self): + # Check examples from + # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm + for i in range(7): + d = cpy_date(2003, 12, 22 + i) + self.assertEqual(d.isocalendar(), (2003, 52, i + 1)) + d = cpy_date(2003, 12, 29) + timedelta(i) + self.assertEqual(d.isocalendar(), (2004, 1, i + 1)) + d = cpy_date(2004, 1, 5 + i) + self.assertEqual(d.isocalendar(), (2004, 2, i + 1)) + d = cpy_date(2009, 12, 21 + i) + self.assertEqual(d.isocalendar(), (2009, 52, i + 1)) + d = cpy_date(2009, 12, 28) + timedelta(i) + self.assertEqual(d.isocalendar(), (2009, 53, i + 1)) + d = cpy_date(2010, 1, 4 + i) + self.assertEqual(d.isocalendar(), (2010, 1, i + 1)) + + def test_isoformat(self): + # test isoformat against expected and cpython equiv. + t = cpy_date(2, 3, 2) + t2 = cpython_date(2, 3, 2) + self.assertEqual(t.isoformat(), "0002-03-02") + self.assertEqual(t.isoformat(), t2.isoformat()) + + @unittest.skip("Skip for CircuitPython - ctime() not implemented for date objects.") + def test_ctime(self): + t = cpy_date(2002, 3, 2) + self.assertEqual(t.ctime(), "Sat Mar 2 00:00:00 2002") + + @unittest.skip( + "Skip for CircuitPython - strftime() not implemented for date objects." + ) + def test_strftime(self): + t = cpy_date(2005, 3, 2) + self.assertEqual(t.strftime("m:%m d:%d y:%y"), "m:03 d:02 y:05") + self.assertEqual(t.strftime(""), "") # SF bug #761337 + # self.assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784 + + self.assertRaises(TypeError, t.strftime) # needs an arg + self.assertRaises(TypeError, t.strftime, "one", "two") # too many args + self.assertRaises(TypeError, t.strftime, 42) # arg wrong type + + # test that unicode input is allowed (issue 2782) + self.assertEqual(t.strftime("%m"), "03") + + # A naive object replaces %z and %Z w/ empty strings. + self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''") + + # make sure that invalid format specifiers are handled correctly + # self.assertRaises(ValueError, t.strftime, "%e") + # self.assertRaises(ValueError, t.strftime, "%") + # self.assertRaises(ValueError, t.strftime, "%#") + + # oh well, some systems just ignore those invalid ones. + # at least, excercise them to make sure that no crashes + # are generated + for f in ["%e", "%", "%#"]: + try: + t.strftime(f) + except ValueError: + pass + + # check that this standard extension works + t.strftime("%f") + + def test_format(self): + dt = cpy_date(2007, 9, 10) + self.assertEqual(dt.__format__(""), str(dt)) + + # check that a derived class's __str__() gets called + class A(cpy_date): + def __str__(self): + return "A" + + a = A(2007, 9, 10) + self.assertEqual(a.__format__(""), "A") + + # check that a derived class's strftime gets called + class B(cpy_date): + def strftime(self, format_spec): + return "B" + + b = B(2007, 9, 10) + self.assertEqual(b.__format__(""), str(dt)) + + # date strftime not implemented in CircuitPython, skip + """for fmt in ["m:%m d:%d y:%y", + "m:%m d:%d y:%y H:%H M:%M S:%S", + "%z %Z", + ]: + self.assertEqual(dt.__format__(fmt), dt.strftime(fmt)) + self.assertEqual(a.__format__(fmt), dt.strftime(fmt)) + self.assertEqual(b.__format__(fmt), 'B')""" + + @unittest.skip( + "Skip for CircuitPython - min/max/resolution not implemented for date objects." + ) + def test_resolution_info(self): + # XXX: Should min and max respect subclassing? + if issubclass(cpy_date, datetime): + expected_class = datetime + else: + expected_class = date + self.assertIsInstance(cpy_date.min, expected_class) + self.assertIsInstance(cpy_date.max, expected_class) + self.assertIsInstance(cpy_date.resolution, timedelta) + self.assertTrue(cpy_date.max > cpy_date.min) + + # TODO: Needs timedelta + @unittest.skip("Skip for CircuitPython - timedelta not implemented.") + def test_extreme_timedelta(self): + big = cpy_date.max - cpy_date.min + # 3652058 days, 23 hours, 59 minutes, 59 seconds, 999999 microseconds + n = (big.days * 24 * 3600 + big.seconds) * 1000000 + big.microseconds + # n == 315537897599999999 ~= 2**58.13 + justasbig = timedelta(0, 0, n) + self.assertEqual(big, justasbig) + self.assertEqual(cpy_date.min + big, cpy_date.max) + self.assertEqual(cpy_date.max - big, cpy_date.min) + + def test_timetuple(self): + for i in range(7): + # January 2, 1956 is a Monday (0) + d = cpy_date(1956, 1, 2 + i) + t = d.timetuple() + d2 = cpython_date(1956, 1, 2 + i) + t2 = d2.timetuple() + self.assertEqual(t, t2) + # February 1, 1956 is a Wednesday (2) + d = cpy_date(1956, 2, 1 + i) + t = d.timetuple() + d2 = cpython_date(1956, 2, 1 + i) + t2 = d2.timetuple() + self.assertEqual(t, t2) + # March 1, 1956 is a Thursday (3), and is the 31+29+1 = 61st day + # of the year. + d = cpy_date(1956, 3, 1 + i) + t = d.timetuple() + d2 = cpython_date(1956, 3, 1 + i) + t2 = d2.timetuple() + self.assertEqual(t, t2) + self.assertEqual(t.tm_year, t2.tm_year) + self.assertEqual(t.tm_mon, t2.tm_mon) + self.assertEqual(t.tm_mday, t2.tm_mday) + self.assertEqual(t.tm_hour, t2.tm_hour) + self.assertEqual(t.tm_min, t2.tm_min) + self.assertEqual(t.tm_sec, t2.tm_sec) + self.assertEqual(t.tm_wday, t2.tm_wday) + self.assertEqual(t.tm_yday, t2.tm_yday) + self.assertEqual(t.tm_isdst, t2.tm_isdst) + + def test_compare(self): + t1 = cpy_date(2, 3, 4) + t2 = cpy_date(2, 3, 4) + self.assertEqual(t1, t2) + self.assertTrue(t1 <= t2) + self.assertTrue(t1 >= t2) + self.assertTrue(not t1 != t2) + self.assertTrue(not t1 < t2) + self.assertTrue(not t1 > t2) + + for args in (3, 3, 3), (2, 4, 4), (2, 3, 5): + t2 = cpy_date(*args) # this is larger than t1 + self.assertTrue(t1 < t2) + self.assertTrue(t2 > t1) + self.assertTrue(t1 <= t2) + self.assertTrue(t2 >= t1) + self.assertTrue(t1 != t2) + self.assertTrue(t2 != t1) + self.assertTrue(not t1 == t2) + self.assertTrue(not t2 == t1) + self.assertTrue(not t1 > t2) + self.assertTrue(not t2 < t1) + self.assertTrue(not t1 >= t2) + self.assertTrue(not t2 <= t1) + + for badarg in OTHERSTUFF: + self.assertEqual(t1 == badarg, False) + self.assertEqual(t1 != badarg, True) + self.assertEqual(badarg == t1, False) + self.assertEqual(badarg != t1, True) + + self.assertRaises(TypeError, lambda: t1 < badarg) + self.assertRaises(TypeError, lambda: t1 > badarg) + self.assertRaises(TypeError, lambda: t1 >= badarg) + self.assertRaises(TypeError, lambda: badarg <= t1) + self.assertRaises(TypeError, lambda: badarg < t1) + self.assertRaises(TypeError, lambda: badarg > t1) + self.assertRaises(TypeError, lambda: badarg >= t1) + + def test_mixed_compare(self): + our = cpy_date(2000, 4, 5) + our2 = cpython_date(2000, 4, 5) + + # Our class can be compared for equality to other classes + self.assertEqual(our == 1, our2 == 1) + self.assertEqual(1 == our, 1 == our2) + self.assertEqual(our != 1, our2 != 1) + self.assertEqual(1 != our, 1 != our2) + + # But the ordering is undefined + self.assertRaises(TypeError, lambda: our < 1) + self.assertRaises(TypeError, lambda: 1 < our) + + # Repeat those tests with a different class + + class SomeClass: + pass + + their = SomeClass() + self.assertEqual(our == their, False) + self.assertEqual(their == our, False) + self.assertEqual(our != their, True) + self.assertEqual(their != our, True) + self.assertRaises(TypeError, lambda: our < their) + self.assertRaises(TypeError, lambda: their < our) + + # However, if the other class explicitly defines ordering + # relative to our class, it is allowed to do so + + class LargerThanAnything: + def __lt__(self, other): + return False + + def __le__(self, other): + return isinstance(other, LargerThanAnything) + + def __eq__(self, other): + return isinstance(other, LargerThanAnything) + + def __ne__(self, other): + return not isinstance(other, LargerThanAnything) + + def __gt__(self, other): + return not isinstance(other, LargerThanAnything) + + def __ge__(self, other): + return True + + their = LargerThanAnything() + self.assertEqual(our == their, False) + self.assertEqual(their == our, False) + self.assertEqual(our != their, True) + self.assertEqual(their != our, True) + self.assertEqual(our < their, True) + self.assertEqual(their < our, False) + + @unittest.skip( + "Skip for CircuitPython - min/max date attributes not implemented yet." + ) + def test_bool(self): + # All dates are considered true. + self.assertTrue(cpy_date.min) + self.assertTrue(cpy_date.max) + + @unittest.skip("Skip for CircuitPython - date strftime not implemented yet.") + def test_strftime_y2k(self): + for y in (1, 49, 70, 99, 100, 999, 1000, 1970): + d = cpy_date(y, 1, 1) + # Issue 13305: For years < 1000, the value is not always + # padded to 4 digits across platforms. The C standard + # assumes year >= 1900, so it does not specify the number + # of digits. + if d.strftime("%Y") != "%04d" % y: + # Year 42 returns '42', not padded + self.assertEqual(d.strftime("%Y"), "%d" % y) + # '0042' is obtained anyway + self.assertEqual(d.strftime("%4Y"), "%04d" % y) + + @unittest.skip("Skip for CircuitPython - date replace not implemented.") + def test_replace(self): + cls = cpy_date + args = [1, 2, 3] + base = cls(*args) + self.assertEqual(base, base.replace()) + + i = 0 + for name, newval in (("year", 2), ("month", 3), ("day", 4)): + newargs = args[:] + newargs[i] = newval + expected = cls(*newargs) + got = base.replace(**{name: newval}) + self.assertEqual(expected, got) + i += 1 + + # Out of bounds. + base = cls(2000, 2, 29) + self.assertRaises(ValueError, base.replace, year=2001) + + def test_subclass_date(self): + class C(cpy_date): + theAnswer = 42 + + def __new__(cls, *args, **kws): + temp = kws.copy() + extra = temp.pop("extra") + result = cpy_date.__new__(cls, *args, **temp) + result.extra = extra + return result + + def newmeth(self, start): + return start + self.year + self.month + + args = 2003, 4, 14 + + dt1 = cpy_date(*args) + dt2 = C(*args, **{"extra": 7}) + + self.assertEqual(dt2.__class__, C) + self.assertEqual(dt2.theAnswer, 42) + self.assertEqual(dt2.extra, 7) + self.assertEqual(dt1.toordinal(), dt2.toordinal()) + self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month - 7) diff --git a/tests/test_datetime.py b/tests/test_datetime.py new file mode 100644 index 0000000..4e472a7 --- /dev/null +++ b/tests/test_datetime.py @@ -0,0 +1,1244 @@ +# SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved. +# SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved. +# SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries +# SPDX-License-Identifier: Python-2.0 +# Implements a subset of https://github.com/python/cpython/blob/master/Lib/test/datetimetester.py +import sys +import unittest +from test import support +from test_date import TestDate + +# CPython standard implementation +from datetime import datetime as cpython_datetime +from datetime import MINYEAR, MAXYEAR + +# CircuitPython subset implementation +sys.path.append("..") +from adafruit_datetime import datetime as cpy_datetime +from adafruit_datetime import timedelta +from adafruit_datetime import tzinfo +from adafruit_datetime import date +from adafruit_datetime import time +from adafruit_datetime import timezone + +# TZinfo test +class FixedOffset(tzinfo): + def __init__(self, offset, name, dstoffset=42): + if isinstance(offset, int): + offset = timedelta(minutes=offset) + if isinstance(dstoffset, int): + dstoffset = timedelta(minutes=dstoffset) + self.__offset = offset + self.__name = name + self.__dstoffset = dstoffset + + def __repr__(self): + return self.__name.lower() + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return self.__dstoffset + + +# ======================================================================= +# Decorator for running a function in a specific timezone, correctly +# resetting it afterwards. + + +def run_with_tz(tz): + def decorator(func): + def inner(*args, **kwds): + try: + tzset = time.tzset + except AttributeError: + raise unittest.SkipTest("tzset required") + if "TZ" in os.environ: + orig_tz = os.environ["TZ"] + else: + orig_tz = None + os.environ["TZ"] = tz + tzset() + + # now run the function, resetting the tz on exceptions + try: + return func(*args, **kwds) + finally: + if orig_tz is None: + del os.environ["TZ"] + else: + os.environ["TZ"] = orig_tz + time.tzset() + + inner.__name__ = func.__name__ + inner.__doc__ = func.__doc__ + return inner + + return decorator + + +class SubclassDatetime(cpy_datetime): + sub_var = 1 + + +class TestDateTime(TestDate): + theclass = cpy_datetime + theclass_cpython = cpython_datetime + + def test_basic_attributes(self): + dt = self.theclass(2002, 3, 1, 12, 0) + dt2 = self.theclass_cpython(2002, 3, 1, 12, 0) + # test circuitpython basic attributes + self.assertEqual(dt.year, 2002) + self.assertEqual(dt.month, 3) + self.assertEqual(dt.day, 1) + self.assertEqual(dt.hour, 12) + self.assertEqual(dt.minute, 0) + self.assertEqual(dt.second, 0) + self.assertEqual(dt.microsecond, 0) + # test circuitpython basic attributes against cpython basic attributes + self.assertEqual(dt.year, dt2.year) + self.assertEqual(dt.month, dt2.month) + self.assertEqual(dt.day, dt2.day) + self.assertEqual(dt.hour, dt2.hour) + self.assertEqual(dt.minute, dt2.minute) + self.assertEqual(dt.second, dt2.second) + self.assertEqual(dt.microsecond, dt2.microsecond) + + def test_basic_attributes_nonzero(self): + # Make sure all attributes are non-zero so bugs in + # bit-shifting access show up. + dt = self.theclass(2002, 3, 1, 12, 59, 59, 8000) + self.assertEqual(dt.year, 2002) + self.assertEqual(dt.month, 3) + self.assertEqual(dt.day, 1) + self.assertEqual(dt.hour, 12) + self.assertEqual(dt.minute, 59) + self.assertEqual(dt.second, 59) + self.assertEqual(dt.microsecond, 8000) + + @unittest.skip("issue with startswith and ada lib.") + def test_roundtrip(self): + for dt in (self.theclass(1, 2, 3, 4, 5, 6, 7), self.theclass.now()): + # Verify dt -> string -> datetime identity. + s = repr(dt) + self.assertTrue(s.startswith("datetime.")) + s = s[9:] + dt2 = eval(s) + self.assertEqual(dt, dt2) + + # Verify identity via reconstructing from pieces. + dt2 = self.theclass( + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond + ) + self.assertEqual(dt, dt2) + + @unittest.skip("isoformat not implemented") + def test_isoformat(self): + t = self.theclass(1, 2, 3, 4, 5, 1, 123) + self.assertEqual(t.isoformat(), "0001-02-03T04:05:01.000123") + self.assertEqual(t.isoformat("T"), "0001-02-03T04:05:01.000123") + self.assertEqual(t.isoformat(" "), "0001-02-03 04:05:01.000123") + self.assertEqual(t.isoformat("\x00"), "0001-02-03\x0004:05:01.000123") + # bpo-34482: Check that surrogates are handled properly. + self.assertEqual(t.isoformat("\ud800"), "0001-02-03\ud80004:05:01.000123") + self.assertEqual(t.isoformat(timespec="hours"), "0001-02-03T04") + self.assertEqual(t.isoformat(timespec="minutes"), "0001-02-03T04:05") + self.assertEqual(t.isoformat(timespec="seconds"), "0001-02-03T04:05:01") + self.assertEqual( + t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.000" + ) + self.assertEqual( + t.isoformat(timespec="microseconds"), "0001-02-03T04:05:01.000123" + ) + self.assertEqual(t.isoformat(timespec="auto"), "0001-02-03T04:05:01.000123") + self.assertEqual(t.isoformat(sep=" ", timespec="minutes"), "0001-02-03 04:05") + self.assertRaises(ValueError, t.isoformat, timespec="foo") + # bpo-34482: Check that surrogates are handled properly. + self.assertRaises(ValueError, t.isoformat, timespec="\ud800") + # str is ISO format with the separator forced to a blank. + self.assertEqual(str(t), "0001-02-03 04:05:01.000123") + + t = self.theclass(1, 2, 3, 4, 5, 1, 999500, tzinfo=timezone.utc) + self.assertEqual( + t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.999+00:00" + ) + + t = self.theclass(1, 2, 3, 4, 5, 1, 999500) + self.assertEqual( + t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.999" + ) + + t = self.theclass(1, 2, 3, 4, 5, 1) + self.assertEqual(t.isoformat(timespec="auto"), "0001-02-03T04:05:01") + self.assertEqual( + t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.000" + ) + self.assertEqual( + t.isoformat(timespec="microseconds"), "0001-02-03T04:05:01.000000" + ) + + t = self.theclass(2, 3, 2) + self.assertEqual(t.isoformat(), "0002-03-02T00:00:00") + self.assertEqual(t.isoformat("T"), "0002-03-02T00:00:00") + self.assertEqual(t.isoformat(" "), "0002-03-02 00:00:00") + # str is ISO format with the separator forced to a blank. + self.assertEqual(str(t), "0002-03-02 00:00:00") + # ISO format with timezone + tz = FixedOffset(timedelta(seconds=16), "XXX") + t = self.theclass(2, 3, 2, tzinfo=tz) + self.assertEqual(t.isoformat(), "0002-03-02T00:00:00+00:00:16") + + @unittest.skip("isoformat not implemented.") + def test_isoformat_timezone(self): + tzoffsets = [ + ("05:00", timedelta(hours=5)), + ("02:00", timedelta(hours=2)), + ("06:27", timedelta(hours=6, minutes=27)), + ("12:32:30", timedelta(hours=12, minutes=32, seconds=30)), + ( + "02:04:09.123456", + timedelta(hours=2, minutes=4, seconds=9, microseconds=123456), + ), + ] + + tzinfos = [ + ("", None), + ("+00:00", timezone.utc), + ("+00:00", timezone(timedelta(0))), + ] + + tzinfos += [ + (prefix + expected, timezone(sign * td)) + for expected, td in tzoffsets + for prefix, sign in [("-", -1), ("+", 1)] + ] + + dt_base = self.theclass(2016, 4, 1, 12, 37, 9) + exp_base = "2016-04-01T12:37:09" + + for exp_tz, tzi in tzinfos: + dt = dt_base.replace(tzinfo=tzi) + exp = exp_base + exp_tz + with self.subTest(tzi=tzi): + assert dt.isoformat() == exp + + @unittest.skip("strftime not implemented in datetime") + def test_format(self): + dt = self.theclass(2007, 9, 10, 4, 5, 1, 123) + self.assertEqual(dt.__format__(""), str(dt)) + + with self.assertRaisesRegex(TypeError, "must be str, not int"): + dt.__format__(123) + + # check that a derived class's __str__() gets called + class A(self.theclass): + def __str__(self): + return "A" + + a = A(2007, 9, 10, 4, 5, 1, 123) + self.assertEqual(a.__format__(""), "A") + + # check that a derived class's strftime gets called + class B(self.theclass): + def strftime(self, format_spec): + return "B" + + b = B(2007, 9, 10, 4, 5, 1, 123) + self.assertEqual(b.__format__(""), str(dt)) + + for fmt in [ + "m:%m d:%d y:%y", + "m:%m d:%d y:%y H:%H M:%M S:%S", + "%z %Z", + ]: + self.assertEqual(dt.__format__(fmt), dt.strftime(fmt)) + self.assertEqual(a.__format__(fmt), dt.strftime(fmt)) + self.assertEqual(b.__format__(fmt), "B") + + @unittest.skip("ctime not implemented") + def test_more_ctime(self): + # Test fields that TestDate doesn't touch. + import time + + t = self.theclass(2002, 3, 2, 18, 3, 5, 123) + self.assertEqual(t.ctime(), "Sat Mar 2 18:03:05 2002") + # Oops! The next line fails on Win2K under MSVC 6, so it's commented + # out. The difference is that t.ctime() produces " 2" for the day, + # but platform ctime() produces "02" for the day. According to + # C99, t.ctime() is correct here. + # self.assertEqual(t.ctime(), time.ctime(time.mktime(t.timetuple()))) + + # So test a case where that difference doesn't matter. + t = self.theclass(2002, 3, 22, 18, 3, 5, 123) + self.assertEqual(t.ctime(), time.ctime(time.mktime(t.timetuple()))) + + def test_tz_independent_comparing(self): + dt1 = self.theclass(2002, 3, 1, 9, 0, 0) + dt2 = self.theclass(2002, 3, 1, 10, 0, 0) + dt3 = self.theclass(2002, 3, 1, 9, 0, 0) + self.assertEqual(dt1, dt3) + self.assertTrue(dt2 > dt3) + + # Make sure comparison doesn't forget microseconds, and isn't done + # via comparing a float timestamp (an IEEE double doesn't have enough + # precision to span microsecond resolution across years 1 through 9999, + # so comparing via timestamp necessarily calls some distinct values + # equal). + dt1 = self.theclass(MAXYEAR, 12, 31, 23, 59, 59, 999998) + us = timedelta(microseconds=1) + dt2 = dt1 + us + self.assertEqual(dt2 - dt1, us) + self.assertTrue(dt1 < dt2) + + @unittest.skip("not implemented - strftime") + def test_strftime_with_bad_tzname_replace(self): + # verify ok if tzinfo.tzname().replace() returns a non-string + class MyTzInfo(FixedOffset): + def tzname(self, dt): + class MyStr(str): + def replace(self, *args): + return None + + return MyStr("name") + + t = self.theclass(2005, 3, 2, 0, 0, 0, 0, MyTzInfo(3, "name")) + self.assertRaises(TypeError, t.strftime, "%Z") + + def test_bad_constructor_arguments(self): + # bad years + self.theclass(MINYEAR, 1, 1) # no exception + self.theclass(MAXYEAR, 1, 1) # no exception + self.assertRaises(ValueError, self.theclass, MINYEAR - 1, 1, 1) + self.assertRaises(ValueError, self.theclass, MAXYEAR + 1, 1, 1) + # bad months + self.theclass(2000, 1, 1) # no exception + self.theclass(2000, 12, 1) # no exception + self.assertRaises(ValueError, self.theclass, 2000, 0, 1) + self.assertRaises(ValueError, self.theclass, 2000, 13, 1) + # bad days + self.theclass(2000, 2, 29) # no exception + self.theclass(2004, 2, 29) # no exception + self.theclass(2400, 2, 29) # no exception + self.assertRaises(ValueError, self.theclass, 2000, 2, 30) + self.assertRaises(ValueError, self.theclass, 2001, 2, 29) + self.assertRaises(ValueError, self.theclass, 2100, 2, 29) + self.assertRaises(ValueError, self.theclass, 1900, 2, 29) + self.assertRaises(ValueError, self.theclass, 2000, 1, 0) + self.assertRaises(ValueError, self.theclass, 2000, 1, 32) + # bad hours + self.theclass(2000, 1, 31, 0) # no exception + self.theclass(2000, 1, 31, 23) # no exception + self.assertRaises(ValueError, self.theclass, 2000, 1, 31, -1) + self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 24) + # bad minutes + self.theclass(2000, 1, 31, 23, 0) # no exception + self.theclass(2000, 1, 31, 23, 59) # no exception + self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, -1) + self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 60) + # bad seconds + self.theclass(2000, 1, 31, 23, 59, 0) # no exception + self.theclass(2000, 1, 31, 23, 59, 59) # no exception + self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, -1) + self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 60) + # bad microseconds + self.theclass(2000, 1, 31, 23, 59, 59, 0) # no exception + self.theclass(2000, 1, 31, 23, 59, 59, 999999) # no exception + self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 59, -1) + self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 59, 1000000) + # bad fold + self.assertRaises(ValueError, self.theclass, 2000, 1, 31, fold=-1) + self.assertRaises(ValueError, self.theclass, 2000, 1, 31, fold=2) + # Positional fold: + self.assertRaises(TypeError, self.theclass, 2000, 1, 31, 23, 59, 59, 0, None, 1) + + def test_hash_equality(self): + d = self.theclass(2000, 12, 31, 23, 30, 17) + e = self.theclass(2000, 12, 31, 23, 30, 17) + self.assertEqual(d, e) + self.assertEqual(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + self.assertEqual(len(dic), 1) + self.assertEqual(dic[d], 2) + self.assertEqual(dic[e], 2) + + d = self.theclass(2001, 1, 1, 0, 5, 17) + e = self.theclass(2001, 1, 1, 0, 5, 17) + self.assertEqual(d, e) + self.assertEqual(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + self.assertEqual(len(dic), 1) + self.assertEqual(dic[d], 2) + self.assertEqual(dic[e], 2) + + def test_computations(self): + a = self.theclass(2002, 1, 31) + b = self.theclass(1956, 1, 31) + diff = a - b + self.assertEqual(diff.days, 46 * 365 + len(range(1956, 2002, 4))) + self.assertEqual(diff.seconds, 0) + self.assertEqual(diff.microseconds, 0) + a = self.theclass(2002, 3, 2, 17, 6) + millisec = timedelta(0, 0, 1000) + hour = timedelta(0, 3600) + day = timedelta(1) + week = timedelta(7) + self.assertEqual(a + hour, self.theclass(2002, 3, 2, 18, 6)) + self.assertEqual(hour + a, self.theclass(2002, 3, 2, 18, 6)) + self.assertEqual(a + 10 * hour, self.theclass(2002, 3, 3, 3, 6)) + self.assertEqual(a - hour, self.theclass(2002, 3, 2, 16, 6)) + self.assertEqual(-hour + a, self.theclass(2002, 3, 2, 16, 6)) + self.assertEqual(a - hour, a + -hour) + self.assertEqual(a - 20 * hour, self.theclass(2002, 3, 1, 21, 6)) + self.assertEqual(a + day, self.theclass(2002, 3, 3, 17, 6)) + self.assertEqual(a - day, self.theclass(2002, 3, 1, 17, 6)) + self.assertEqual(a + week, self.theclass(2002, 3, 9, 17, 6)) + self.assertEqual(a - week, self.theclass(2002, 2, 23, 17, 6)) + self.assertEqual(a + 52 * week, self.theclass(2003, 3, 1, 17, 6)) + self.assertEqual(a - 52 * week, self.theclass(2001, 3, 3, 17, 6)) + self.assertEqual((a + week) - a, week) + self.assertEqual((a + day) - a, day) + self.assertEqual((a + hour) - a, hour) + self.assertEqual((a + millisec) - a, millisec) + self.assertEqual((a - week) - a, -week) + self.assertEqual((a - day) - a, -day) + self.assertEqual((a - hour) - a, -hour) + self.assertEqual((a - millisec) - a, -millisec) + self.assertEqual(a - (a + week), -week) + self.assertEqual(a - (a + day), -day) + self.assertEqual(a - (a + hour), -hour) + self.assertEqual(a - (a + millisec), -millisec) + self.assertEqual(a - (a - week), week) + self.assertEqual(a - (a - day), day) + self.assertEqual(a - (a - hour), hour) + self.assertEqual(a - (a - millisec), millisec) + self.assertEqual( + a + (week + day + hour + millisec), + self.theclass(2002, 3, 10, 18, 6, 0, 1000), + ) + self.assertEqual( + a + (week + day + hour + millisec), (((a + week) + day) + hour) + millisec + ) + self.assertEqual( + a - (week + day + hour + millisec), + self.theclass(2002, 2, 22, 16, 5, 59, 999000), + ) + self.assertEqual( + a - (week + day + hour + millisec), (((a - week) - day) - hour) - millisec + ) + # Add/sub ints or floats should be illegal + for i in 1, 1.0: + self.assertRaises(TypeError, lambda: a + i) + self.assertRaises(TypeError, lambda: a - i) + self.assertRaises(TypeError, lambda: i + a) + self.assertRaises(TypeError, lambda: i - a) + + # delta - datetime is senseless. + self.assertRaises(TypeError, lambda: day - a) + # mixing datetime and (delta or datetime) via * or // is senseless + self.assertRaises(TypeError, lambda: day * a) + self.assertRaises(TypeError, lambda: a * day) + self.assertRaises(TypeError, lambda: day // a) + self.assertRaises(TypeError, lambda: a // day) + self.assertRaises(TypeError, lambda: a * a) + self.assertRaises(TypeError, lambda: a // a) + # datetime + datetime is senseless + self.assertRaises(TypeError, lambda: a + a) + + def test_more_compare(self): + # The test_compare() inherited from TestDate covers the error cases. + # We just want to test lexicographic ordering on the members datetime + # has that date lacks. + args = [2000, 11, 29, 20, 58, 16, 999998] + t1 = self.theclass(*args) + t2 = self.theclass(*args) + self.assertEqual(t1, t2) + self.assertTrue(t1 <= t2) + self.assertTrue(t1 >= t2) + self.assertFalse(t1 != t2) + self.assertFalse(t1 < t2) + self.assertFalse(t1 > t2) + + for i in range(len(args)): + newargs = args[:] + newargs[i] = args[i] + 1 + t2 = self.theclass(*newargs) # this is larger than t1 + self.assertTrue(t1 < t2) + self.assertTrue(t2 > t1) + self.assertTrue(t1 <= t2) + self.assertTrue(t2 >= t1) + self.assertTrue(t1 != t2) + self.assertTrue(t2 != t1) + self.assertFalse(t1 == t2) + self.assertFalse(t2 == t1) + self.assertFalse(t1 > t2) + self.assertFalse(t2 < t1) + self.assertFalse(t1 >= t2) + self.assertFalse(t2 <= t1) + + # A helper for timestamp constructor tests. + def verify_field_equality(self, expected, got): + self.assertEqual(expected.tm_year, got.year) + self.assertEqual(expected.tm_mon, got.month) + self.assertEqual(expected.tm_mday, got.day) + self.assertEqual(expected.tm_hour, got.hour) + self.assertEqual(expected.tm_min, got.minute) + self.assertEqual(expected.tm_sec, got.second) + + def test_fromtimestamp(self): + import time + + ts = time.time() + expected = time.localtime(ts) + got = self.theclass.fromtimestamp(ts) + self.verify_field_equality(expected, got) + + def test_utcfromtimestamp(self): + import time + + ts = time.time() + expected = time.gmtime(ts) + got = self.theclass.utcfromtimestamp(ts) + self.verify_field_equality(expected, got) + + # TODO + @unittest.skip("Wait until we bring in UTCOFFSET") + # Run with US-style DST rules: DST begins 2 a.m. on second Sunday in + # March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0). + @support.run_with_tz("EST+05EDT,M3.2.0,M11.1.0") + def test_timestamp_naive(self): + t = self.theclass(1970, 1, 1) + self.assertEqual(t.timestamp(), 18000.0) + t = self.theclass(1970, 1, 1, 1, 2, 3, 4) + self.assertEqual(t.timestamp(), 18000.0 + 3600 + 2 * 60 + 3 + 4 * 1e-6) + # Missing hour + t0 = self.theclass(2012, 3, 11, 2, 30) + t1 = t0.replace(fold=1) + self.assertEqual( + self.theclass.fromtimestamp(t1.timestamp()), t0 - timedelta(hours=1) + ) + self.assertEqual( + self.theclass.fromtimestamp(t0.timestamp()), t1 + timedelta(hours=1) + ) + # Ambiguous hour defaults to DST + t = self.theclass(2012, 11, 4, 1, 30) + self.assertEqual(self.theclass.fromtimestamp(t.timestamp()), t) + + # Timestamp may raise an overflow error on some platforms + # XXX: Do we care to support the first and last year? + for t in [self.theclass(2, 1, 1), self.theclass(9998, 12, 12)]: + try: + s = t.timestamp() + except OverflowError: + pass + else: + self.assertEqual(self.theclass.fromtimestamp(s), t) + + # TODO + @unittest.skip("Hold off on this test until we bring timezone in") + def test_timestamp_aware(self): + t = self.theclass(1970, 1, 1, tzinfo=timezone.utc) + self.assertEqual(t.timestamp(), 0.0) + t = self.theclass(1970, 1, 1, 1, 2, 3, 4, tzinfo=timezone.utc) + self.assertEqual(t.timestamp(), 3600 + 2 * 60 + 3 + 4 * 1e-6) + t = self.theclass( + 1970, 1, 1, 1, 2, 3, 4, tzinfo=timezone(timedelta(hours=-5), "EST") + ) + self.assertEqual(t.timestamp(), 18000 + 3600 + 2 * 60 + 3 + 4 * 1e-6) + + @support.run_with_tz("MSK-03") # Something east of Greenwich + def test_microsecond_rounding(self): + for fts in [self.theclass.fromtimestamp, self.theclass.utcfromtimestamp]: + zero = fts(0) + self.assertEqual(zero.second, 0) + self.assertEqual(zero.microsecond, 0) + one = fts(1e-6) + try: + minus_one = fts(-1e-6) + except OSError: + # localtime(-1) and gmtime(-1) is not supported on Windows + pass + else: + self.assertEqual(minus_one.second, 59) + self.assertEqual(minus_one.microsecond, 999999) + + t = fts(-1e-8) + self.assertEqual(t, zero) + t = fts(-9e-7) + self.assertEqual(t, minus_one) + t = fts(-1e-7) + self.assertEqual(t, zero) + t = fts(-1 / 2 ** 7) + self.assertEqual(t.second, 59) + self.assertEqual(t.microsecond, 992188) + + t = fts(1e-7) + self.assertEqual(t, zero) + t = fts(9e-7) + self.assertEqual(t, one) + t = fts(0.99999949) + self.assertEqual(t.second, 0) + self.assertEqual(t.microsecond, 999999) + t = fts(0.9999999) + self.assertEqual(t.second, 1) + self.assertEqual(t.microsecond, 0) + t = fts(1 / 2 ** 7) + self.assertEqual(t.second, 0) + self.assertEqual(t.microsecond, 7812) + + # TODO + @unittest.skip("timezone not implemented") + def test_timestamp_limits(self): + # minimum timestamp + min_dt = self.theclass.min.replace(tzinfo=timezone.utc) + min_ts = min_dt.timestamp() + try: + # date 0001-01-01 00:00:00+00:00: timestamp=-62135596800 + self.assertEqual( + self.theclass.fromtimestamp(min_ts, tz=timezone.utc), min_dt + ) + except (OverflowError, OSError) as exc: + # the date 0001-01-01 doesn't fit into 32-bit time_t, + # or platform doesn't support such very old date + self.skipTest(str(exc)) + + # maximum timestamp: set seconds to zero to avoid rounding issues + max_dt = self.theclass.max.replace(tzinfo=timezone.utc, second=0, microsecond=0) + max_ts = max_dt.timestamp() + # date 9999-12-31 23:59:00+00:00: timestamp 253402300740 + self.assertEqual(self.theclass.fromtimestamp(max_ts, tz=timezone.utc), max_dt) + + # number of seconds greater than 1 year: make sure that the new date + # is not valid in datetime.datetime limits + delta = 3600 * 24 * 400 + + # too small + ts = min_ts - delta + # converting a Python int to C time_t can raise a OverflowError, + # especially on 32-bit platforms. + with self.assertRaises((ValueError, OverflowError)): + self.theclass.fromtimestamp(ts) + with self.assertRaises((ValueError, OverflowError)): + self.theclass.utcfromtimestamp(ts) + + # too big + ts = max_dt.timestamp() + delta + with self.assertRaises((ValueError, OverflowError)): + self.theclass.fromtimestamp(ts) + with self.assertRaises((ValueError, OverflowError)): + self.theclass.utcfromtimestamp(ts) + + def test_insane_fromtimestamp(self): + # It's possible that some platform maps time_t to double, + # and that this test will fail there. This test should + # exempt such platforms (provided they return reasonable + # results!). + for insane in -1e200, 1e200: + self.assertRaises(OverflowError, self.theclass.fromtimestamp, insane) + + def test_insane_utcfromtimestamp(self): + # It's possible that some platform maps time_t to double, + # and that this test will fail there. This test should + # exempt such platforms (provided they return reasonable + # results!). + for insane in -1e200, 1e200: + self.assertRaises(OverflowError, self.theclass.utcfromtimestamp, insane) + + @unittest.skip("Not implemented - utcnow") + def test_utcnow(self): + import time + + # Call it a success if utcnow() and utcfromtimestamp() are within + # a second of each other. + tolerance = timedelta(seconds=1) + for dummy in range(3): + from_now = self.theclass.utcnow() + from_timestamp = self.theclass.utcfromtimestamp(time.time()) + if abs(from_timestamp - from_now) <= tolerance: + break + # Else try again a few times. + self.assertLessEqual(abs(from_timestamp - from_now), tolerance) + + @unittest.skip("Not implemented - strptime") + def test_strptime(self): + string = "2004-12-01 13:02:47.197" + format = "%Y-%m-%d %H:%M:%S.%f" + expected = _strptime._strptime_datetime(self.theclass, string, format) + got = self.theclass.strptime(string, format) + self.assertEqual(expected, got) + self.assertIs(type(expected), self.theclass) + self.assertIs(type(got), self.theclass) + + # bpo-34482: Check that surrogates are handled properly. + inputs = [ + ("2004-12-01\ud80013:02:47.197", "%Y-%m-%d\ud800%H:%M:%S.%f"), + ("2004\ud80012-01 13:02:47.197", "%Y\ud800%m-%d %H:%M:%S.%f"), + ("2004-12-01 13:02\ud80047.197", "%Y-%m-%d %H:%M\ud800%S.%f"), + ] + for string, format in inputs: + with self.subTest(string=string, format=format): + expected = _strptime._strptime_datetime(self.theclass, string, format) + got = self.theclass.strptime(string, format) + self.assertEqual(expected, got) + + strptime = self.theclass.strptime + + self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE) + self.assertEqual(strptime("-0002", "%z").utcoffset(), -2 * MINUTE) + self.assertEqual( + strptime("-00:02:01.000003", "%z").utcoffset(), + -timedelta(minutes=2, seconds=1, microseconds=3), + ) + # Only local timezone and UTC are supported + for tzseconds, tzname in ( + (0, "UTC"), + (0, "GMT"), + (-_time.timezone, _time.tzname[0]), + ): + if tzseconds < 0: + sign = "-" + seconds = -tzseconds + else: + sign = "+" + seconds = tzseconds + hours, minutes = divmod(seconds // 60, 60) + dtstr = "{}{:02d}{:02d} {}".format(sign, hours, minutes, tzname) + dt = strptime(dtstr, "%z %Z") + self.assertEqual(dt.utcoffset(), timedelta(seconds=tzseconds)) + self.assertEqual(dt.tzname(), tzname) + # Can produce inconsistent datetime + dtstr, fmt = "+1234 UTC", "%z %Z" + dt = strptime(dtstr, fmt) + self.assertEqual(dt.utcoffset(), 12 * HOUR + 34 * MINUTE) + self.assertEqual(dt.tzname(), "UTC") + # yet will roundtrip + self.assertEqual(dt.strftime(fmt), dtstr) + + # Produce naive datetime if no %z is provided + self.assertEqual(strptime("UTC", "%Z").tzinfo, None) + + with self.assertRaises(ValueError): + strptime("-2400", "%z") + with self.assertRaises(ValueError): + strptime("-000", "%z") + + @unittest.skip("Not implemented - strptime") + def test_strptime_single_digit(self): + # bpo-34903: Check that single digit dates and times are allowed. + + strptime = self.theclass.strptime + + with self.assertRaises(ValueError): + # %y does require two digits. + newdate = strptime("01/02/3 04:05:06", "%d/%m/%y %H:%M:%S") + dt1 = self.theclass(2003, 2, 1, 4, 5, 6) + dt2 = self.theclass(2003, 1, 2, 4, 5, 6) + dt3 = self.theclass(2003, 2, 1, 0, 0, 0) + dt4 = self.theclass(2003, 1, 25, 0, 0, 0) + inputs = [ + ("%d", "1/02/03 4:5:6", "%d/%m/%y %H:%M:%S", dt1), + ("%m", "01/2/03 4:5:6", "%d/%m/%y %H:%M:%S", dt1), + ("%H", "01/02/03 4:05:06", "%d/%m/%y %H:%M:%S", dt1), + ("%M", "01/02/03 04:5:06", "%d/%m/%y %H:%M:%S", dt1), + ("%S", "01/02/03 04:05:6", "%d/%m/%y %H:%M:%S", dt1), + ("%j", "2/03 04am:05:06", "%j/%y %I%p:%M:%S", dt2), + ("%I", "02/03 4am:05:06", "%j/%y %I%p:%M:%S", dt2), + ("%w", "6/04/03", "%w/%U/%y", dt3), + # %u requires a single digit. + ("%W", "6/4/2003", "%u/%W/%Y", dt3), + ("%V", "6/4/2003", "%u/%V/%G", dt4), + ] + for reason, string, format, target in inputs: + reason = "test single digit " + reason + with self.subTest( + reason=reason, string=string, format=format, target=target + ): + newdate = strptime(string, format) + self.assertEqual(newdate, target, msg=reason) + + def test_more_timetuple(self): + # This tests fields beyond those tested by the TestDate.test_timetuple. + t = self.theclass(2004, 12, 31, 6, 22, 33) + self.assertEqual(t.timetuple(), (2004, 12, 31, 6, 22, 33, 4, 366, -1)) + self.assertEqual( + t.timetuple(), + ( + t.year, + t.month, + t.day, + t.hour, + t.minute, + t.second, + t.weekday(), + t.toordinal() - date(t.year, 1, 1).toordinal() + 1, + -1, + ), + ) + tt = t.timetuple() + self.assertEqual(tt.tm_year, t.year) + self.assertEqual(tt.tm_mon, t.month) + self.assertEqual(tt.tm_mday, t.day) + self.assertEqual(tt.tm_hour, t.hour) + self.assertEqual(tt.tm_min, t.minute) + self.assertEqual(tt.tm_sec, t.second) + self.assertEqual(tt.tm_wday, t.weekday()) + self.assertEqual(tt.tm_yday, t.toordinal() - date(t.year, 1, 1).toordinal() + 1) + self.assertEqual(tt.tm_isdst, -1) + + @unittest.skip("Not implemented - strftime") + def test_more_strftime(self): + # This tests fields beyond those tested by the TestDate.test_strftime. + t = self.theclass(2004, 12, 31, 6, 22, 33, 47) + self.assertEqual( + t.strftime("%m %d %y %f %S %M %H %j"), "12 31 04 000047 33 22 06 366" + ) + for (s, us), z in [ + ((33, 123), "33.000123"), + ((33, 0), "33"), + ]: + tz = timezone(-timedelta(hours=2, seconds=s, microseconds=us)) + t = t.replace(tzinfo=tz) + self.assertEqual(t.strftime("%z"), "-0200" + z) + + # bpo-34482: Check that surrogates don't cause a crash. + try: + t.strftime("%y\ud800%m %H\ud800%M") + except UnicodeEncodeError: + pass + + def test_extract(self): + dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234) + self.assertEqual(dt.date(), date(2002, 3, 4)) + self.assertEqual(dt.time(), time(18, 45, 3, 1234)) + + # TODO + @unittest.skip("not implemented - timezone") + def test_combine(self): + d = date(2002, 3, 4) + t = time(18, 45, 3, 1234) + expected = self.theclass(2002, 3, 4, 18, 45, 3, 1234) + combine = self.theclass.combine + dt = combine(d, t) + self.assertEqual(dt, expected) + + dt = combine(time=t, date=d) + self.assertEqual(dt, expected) + + self.assertEqual(d, dt.date()) + self.assertEqual(t, dt.time()) + self.assertEqual(dt, combine(dt.date(), dt.time())) + + self.assertRaises(TypeError, combine) # need an arg + self.assertRaises(TypeError, combine, d) # need two args + self.assertRaises(TypeError, combine, t, d) # args reversed + self.assertRaises(TypeError, combine, d, t, 1) # wrong tzinfo type + self.assertRaises(TypeError, combine, d, t, 1, 2) # too many args + self.assertRaises(TypeError, combine, "date", "time") # wrong types + self.assertRaises(TypeError, combine, d, "time") # wrong type + self.assertRaises(TypeError, combine, "date", t) # wrong type + + # tzinfo= argument + dt = combine(d, t, timezone.utc) + self.assertIs(dt.tzinfo, timezone.utc) + dt = combine(d, t, tzinfo=timezone.utc) + self.assertIs(dt.tzinfo, timezone.utc) + t = time() + dt = combine(dt, t) + self.assertEqual(dt.date(), d) + self.assertEqual(dt.time(), t) + + def test_replace(self): + cls = self.theclass + args = [1, 2, 3, 4, 5, 6, 7] + base = cls(*args) + self.assertEqual(base, base.replace()) + + i = 0 + for name, newval in ( + ("year", 2), + ("month", 3), + ("day", 4), + ("hour", 5), + ("minute", 6), + ("second", 7), + ("microsecond", 8), + ): + newargs = args[:] + newargs[i] = newval + expected = cls(*newargs) + got = base.replace(**{name: newval}) + self.assertEqual(expected, got) + i += 1 + + # Out of bounds. + base = cls(2000, 2, 29) + self.assertRaises(ValueError, base.replace, year=2001) + + @unittest.skip("astimezone not impld") + @support.run_with_tz("EDT4") + def test_astimezone(self): + dt = self.theclass.now() + f = FixedOffset(44, "0044") + dt_utc = dt.replace(tzinfo=timezone(timedelta(hours=-4), "EDT")) + self.assertEqual(dt.astimezone(), dt_utc) # naive + self.assertRaises(TypeError, dt.astimezone, f, f) # too many args + self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type + dt_f = dt.replace(tzinfo=f) + timedelta(hours=4, minutes=44) + self.assertEqual(dt.astimezone(f), dt_f) # naive + self.assertEqual(dt.astimezone(tz=f), dt_f) # naive + + class Bogus(tzinfo): + def utcoffset(self, dt): + return None + + def dst(self, dt): + return timedelta(0) + + bog = Bogus() + self.assertRaises(ValueError, dt.astimezone, bog) # naive + self.assertEqual(dt.replace(tzinfo=bog).astimezone(f), dt_f) + + class AlsoBogus(tzinfo): + def utcoffset(self, dt): + return timedelta(0) + + def dst(self, dt): + return None + + alsobog = AlsoBogus() + self.assertRaises(ValueError, dt.astimezone, alsobog) # also naive + + class Broken(tzinfo): + def utcoffset(self, dt): + return 1 + + def dst(self, dt): + return 1 + + broken = Broken() + dt_broken = dt.replace(tzinfo=broken) + with self.assertRaises(TypeError): + dt_broken.astimezone() + + def test_subclass_datetime(self): + class C(self.theclass): + theAnswer = 42 + + def __new__(cls, *args, **kws): + temp = kws.copy() + extra = temp.pop("extra") + result = self.theclass.__new__(cls, *args, **temp) + result.extra = extra + return result + + def newmeth(self, start): + return start + self.year + self.month + self.second + + args = 2003, 4, 14, 12, 13, 41 + + dt1 = self.theclass(*args) + dt2 = C(*args, **{"extra": 7}) + + self.assertEqual(dt2.__class__, C) + self.assertEqual(dt2.theAnswer, 42) + self.assertEqual(dt2.extra, 7) + self.assertEqual(dt1.toordinal(), dt2.toordinal()) + self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month + dt1.second - 7) + + # TODO + @unittest.skip("timezone not implemented") + def test_subclass_alternate_constructors_datetime(self): + # Test that alternate constructors call the constructor + class DateTimeSubclass(self.theclass): + def __new__(cls, *args, **kwargs): + result = self.theclass.__new__(cls, *args, **kwargs) + result.extra = 7 + + return result + + args = (2003, 4, 14, 12, 30, 15, 123456) + d_isoformat = "2003-04-14T12:30:15.123456" # Equivalent isoformat() + utc_ts = 1050323415.123456 # UTC timestamp + + base_d = DateTimeSubclass(*args) + self.assertIsInstance(base_d, DateTimeSubclass) + self.assertEqual(base_d.extra, 7) + + # Timestamp depends on time zone, so we'll calculate the equivalent here + ts = base_d.timestamp() + + test_cases = [ + ("fromtimestamp", (ts,), base_d), + # See https://bugs.python.org/issue32417 + ("fromtimestamp", (ts, timezone.utc), base_d.astimezone(timezone.utc)), + ("utcfromtimestamp", (utc_ts,), base_d), + ("fromisoformat", (d_isoformat,), base_d), + ("strptime", (d_isoformat, "%Y-%m-%dT%H:%M:%S.%f"), base_d), + ("combine", (date(*args[0:3]), time(*args[3:])), base_d), + ] + + for constr_name, constr_args, expected in test_cases: + for base_obj in (DateTimeSubclass, base_d): + # Test both the classmethod and method + with self.subTest( + base_obj_type=type(base_obj), constr_name=constr_name + ): + constructor = getattr(base_obj, constr_name) + + dt = constructor(*constr_args) + + # Test that it creates the right subclass + self.assertIsInstance(dt, DateTimeSubclass) + + # Test that it's equal to the base object + self.assertEqual(dt, expected) + + # Test that it called the constructor + self.assertEqual(dt.extra, 7) + + # TODO + @unittest.skip("timezone not implemented") + def test_subclass_now(self): + # Test that alternate constructors call the constructor + class DateTimeSubclass(self.theclass): + def __new__(cls, *args, **kwargs): + result = self.theclass.__new__(cls, *args, **kwargs) + result.extra = 7 + + return result + + test_cases = [ + ("now", "now", {}), + ("utcnow", "utcnow", {}), + ("now_utc", "now", {"tz": timezone.utc}), + ("now_fixed", "now", {"tz": timezone(timedelta(hours=-5), "EST")}), + ] + + for name, meth_name, kwargs in test_cases: + with self.subTest(name): + constr = getattr(DateTimeSubclass, meth_name) + dt = constr(**kwargs) + + self.assertIsInstance(dt, DateTimeSubclass) + self.assertEqual(dt.extra, 7) + + # TODO + @unittest.skip("timezone not implemented") + def test_fromisoformat_datetime(self): + # Test that isoformat() is reversible + base_dates = [(1, 1, 1), (1900, 1, 1), (2004, 11, 12), (2017, 5, 30)] + + base_times = [ + (0, 0, 0, 0), + (0, 0, 0, 241000), + (0, 0, 0, 234567), + (12, 30, 45, 234567), + ] + + separators = [" ", "T"] + + tzinfos = [ + None, + timezone.utc, + timezone(timedelta(hours=-5)), + timezone(timedelta(hours=2)), + ] + + dts = [ + self.theclass(*date_tuple, *time_tuple, tzinfo=tzi) + for date_tuple in base_dates + for time_tuple in base_times + for tzi in tzinfos + ] + + for dt in dts: + for sep in separators: + dtstr = dt.isoformat(sep=sep) + + with self.subTest(dtstr=dtstr): + dt_rt = self.theclass.fromisoformat(dtstr) + self.assertEqual(dt, dt_rt) + + # TODO + @unittest.skip("not implemented timezone") + def test_fromisoformat_timezone(self): + base_dt = self.theclass(2014, 12, 30, 12, 30, 45, 217456) + + tzoffsets = [ + timedelta(hours=5), + timedelta(hours=2), + timedelta(hours=6, minutes=27), + timedelta(hours=12, minutes=32, seconds=30), + timedelta(hours=2, minutes=4, seconds=9, microseconds=123456), + ] + + tzoffsets += [-1 * td for td in tzoffsets] + + tzinfos = [None, timezone.utc, timezone(timedelta(hours=0))] + + tzinfos += [timezone(td) for td in tzoffsets] + + for tzi in tzinfos: + dt = base_dt.replace(tzinfo=tzi) + dtstr = dt.isoformat() + + with self.subTest(tstr=dtstr): + dt_rt = self.theclass.fromisoformat(dtstr) + assert dt == dt_rt, dt_rt + + # TODO + @unittest.skip("fromisoformat not implemented") + def test_fromisoformat_separators(self): + separators = [ + " ", + "T", + "\u007f", # 1-bit widths + "\u0080", + "ʁ", # 2-bit widths + "ᛇ", + "時", # 3-bit widths + "🐍", # 4-bit widths + "\ud800", # bpo-34454: Surrogate code point + ] + + for sep in separators: + dt = self.theclass(2018, 1, 31, 23, 59, 47, 124789) + dtstr = dt.isoformat(sep=sep) + + with self.subTest(dtstr=dtstr): + dt_rt = self.theclass.fromisoformat(dtstr) + self.assertEqual(dt, dt_rt) + + # TODO + @unittest.skip("fromisoformat not implemented") + def test_fromisoformat_ambiguous(self): + # Test strings like 2018-01-31+12:15 (where +12:15 is not a time zone) + separators = ["+", "-"] + for sep in separators: + dt = self.theclass(2018, 1, 31, 12, 15) + dtstr = dt.isoformat(sep=sep) + + with self.subTest(dtstr=dtstr): + dt_rt = self.theclass.fromisoformat(dtstr) + self.assertEqual(dt, dt_rt) + + # TODO + @unittest.skip("fromisoformat not implemented") + def test_fromisoformat_timespecs(self): + datetime_bases = [(2009, 12, 4, 8, 17, 45, 123456), (2009, 12, 4, 8, 17, 45, 0)] + + tzinfos = [ + None, + timezone.utc, + timezone(timedelta(hours=-5)), + timezone(timedelta(hours=2)), + timezone(timedelta(hours=6, minutes=27)), + ] + + timespecs = ["hours", "minutes", "seconds", "milliseconds", "microseconds"] + + for ip, ts in enumerate(timespecs): + for tzi in tzinfos: + for dt_tuple in datetime_bases: + if ts == "milliseconds": + new_microseconds = 1000 * (dt_tuple[6] // 1000) + dt_tuple = dt_tuple[0:6] + (new_microseconds,) + + dt = self.theclass(*(dt_tuple[0 : (4 + ip)]), tzinfo=tzi) + dtstr = dt.isoformat(timespec=ts) + with self.subTest(dtstr=dtstr): + dt_rt = self.theclass.fromisoformat(dtstr) + self.assertEqual(dt, dt_rt) + + # TODO + @unittest.skip("fromisoformat not implemented") + def test_fromisoformat_fails_datetime(self): + # Test that fromisoformat() fails on invalid values + bad_strs = [ + "", # Empty string + "\ud800", # bpo-34454: Surrogate code point + "2009.04-19T03", # Wrong first separator + "2009-04.19T03", # Wrong second separator + "2009-04-19T0a", # Invalid hours + "2009-04-19T03:1a:45", # Invalid minutes + "2009-04-19T03:15:4a", # Invalid seconds + "2009-04-19T03;15:45", # Bad first time separator + "2009-04-19T03:15;45", # Bad second time separator + "2009-04-19T03:15:4500:00", # Bad time zone separator + "2009-04-19T03:15:45.2345", # Too many digits for milliseconds + "2009-04-19T03:15:45.1234567", # Too many digits for microseconds + "2009-04-19T03:15:45.123456+24:30", # Invalid time zone offset + "2009-04-19T03:15:45.123456-24:30", # Invalid negative offset + "2009-04-10ᛇᛇᛇᛇᛇ12:15", # Too many unicode separators + "2009-04\ud80010T12:15", # Surrogate char in date + "2009-04-10T12\ud80015", # Surrogate char in time + "2009-04-19T1", # Incomplete hours + "2009-04-19T12:3", # Incomplete minutes + "2009-04-19T12:30:4", # Incomplete seconds + "2009-04-19T12:", # Ends with time separator + "2009-04-19T12:30:", # Ends with time separator + "2009-04-19T12:30:45.", # Ends with time separator + "2009-04-19T12:30:45.123456+", # Ends with timzone separator + "2009-04-19T12:30:45.123456-", # Ends with timzone separator + "2009-04-19T12:30:45.123456-05:00a", # Extra text + "2009-04-19T12:30:45.123-05:00a", # Extra text + "2009-04-19T12:30:45-05:00a", # Extra text + ] + + for bad_str in bad_strs: + with self.subTest(bad_str=bad_str): + with self.assertRaises(ValueError): + self.theclass.fromisoformat(bad_str) + + # TODO + @unittest.skip("fromisoformat not implemented") + def test_fromisoformat_fails_surrogate(self): + # Test that when fromisoformat() fails with a surrogate character as + # the separator, the error message contains the original string + dtstr = "2018-01-03\ud80001:0113" + + with self.assertRaisesRegex(ValueError, re.escape(repr(dtstr))): + self.theclass.fromisoformat(dtstr) + + # TODO + @unittest.skip("fromisoformat not implemented") + def test_fromisoformat_utc(self): + dt_str = "2014-04-19T13:21:13+00:00" + dt = self.theclass.fromisoformat(dt_str) + + self.assertIs(dt.tzinfo, timezone.utc) + + # TODO + @unittest.skip("fromisoformat not implemented") + def test_fromisoformat_subclass(self): + class DateTimeSubclass(self.theclass): + pass + + dt = DateTimeSubclass( + 2014, + 12, + 14, + 9, + 30, + 45, + 457390, + tzinfo=timezone(timedelta(hours=10, minutes=45)), + ) + + dt_rt = DateTimeSubclass.fromisoformat(dt.isoformat()) + + self.assertEqual(dt, dt_rt) + self.assertIsInstance(dt_rt, DateTimeSubclass) diff --git a/tests/test_time.py b/tests/test_time.py new file mode 100644 index 0000000..6ef4c1e --- /dev/null +++ b/tests/test_time.py @@ -0,0 +1,386 @@ +# SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved. +# SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. +# All rights reserved. +# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved. +# SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries +# SPDX-License-Identifier: Python-2.0 +# Implements a subset of https://github.com/python/cpython/blob/master/Lib/test/datetimetester.py +import unittest + +# CPython standard implementation +from datetime import time as cpython_time + +# CircuitPython subset implementation +import sys + +sys.path.append("..") +from adafruit_datetime import time as cpy_time + +# An arbitrary collection of objects of non-datetime types, for testing +# mixed-type comparisons. +OTHERSTUFF = (10, 34.5, "abc", {}, [], ()) + +############################################################################# +# Base class for testing a particular aspect of timedelta, time, date and +# datetime comparisons. + + +class HarmlessMixedComparison: + # Test that __eq__ and __ne__ don't complain for mixed-type comparisons. + + # Subclasses must define 'theclass', and theclass(1, 1, 1) must be a + # legit constructor. + + def test_harmless_mixed_comparison(self): + me = self.theclass(1, 1, 1) + + self.assertFalse(me == ()) + self.assertTrue(me != ()) + self.assertFalse(() == me) + self.assertTrue(() != me) + + self.assertIn(me, [1, 20, [], me]) + self.assertIn([], [me, 1, 20, []]) + + def test_harmful_mixed_comparison(self): + me = self.theclass(1, 1, 1) + + self.assertRaises(TypeError, lambda: me < ()) + self.assertRaises(TypeError, lambda: me <= ()) + self.assertRaises(TypeError, lambda: me > ()) + self.assertRaises(TypeError, lambda: me >= ()) + + self.assertRaises(TypeError, lambda: () < me) + self.assertRaises(TypeError, lambda: () <= me) + self.assertRaises(TypeError, lambda: () > me) + self.assertRaises(TypeError, lambda: () >= me) + + +class TestTime(HarmlessMixedComparison, unittest.TestCase): + + theclass = cpy_time + theclass_cpython = cpython_time + + def test_basic_attributes(self): + t = self.theclass(12, 0) + t2 = self.theclass_cpython(12, 0) + # Check adafruit_datetime module + self.assertEqual(t.hour, 12) + self.assertEqual(t.minute, 0) + self.assertEqual(t.second, 0) + self.assertEqual(t.microsecond, 0) + # Validate against CPython datetime module + self.assertEqual(t.hour, t2.hour) + self.assertEqual(t.minute, t2.minute) + self.assertEqual(t.second, t2.second) + self.assertEqual(t.microsecond, t2.microsecond) + + def test_basic_attributes_nonzero(self): + # Make sure all attributes are non-zero so bugs in + # bit-shifting access show up. + t = self.theclass(12, 59, 59, 8000) + t2 = self.theclass_cpython(12, 59, 59, 8000) + # Check adafruit_datetime module + self.assertEqual(t.hour, 12) + self.assertEqual(t.minute, 59) + self.assertEqual(t.second, 59) + self.assertEqual(t.microsecond, 8000) + # Validate against CPython datetime module + self.assertEqual(t.hour, t2.hour) + self.assertEqual(t.minute, t2.minute) + self.assertEqual(t.second, t2.second) + self.assertEqual(t.microsecond, t2.microsecond) + + def test_comparing(self): + args = [1, 2, 3, 4] + t1 = self.theclass(*args) + t2 = self.theclass(*args) + self.assertEqual(t1, t2) + self.assertTrue(t1 <= t2) + self.assertTrue(t1 >= t2) + self.assertTrue(not t1 != t2) + self.assertTrue(not t1 < t2) + self.assertTrue(not t1 > t2) + + for i in range(len(args)): + newargs = args[:] + newargs[i] = args[i] + 1 + t2 = self.theclass(*newargs) # this is larger than t1 + self.assertTrue(t1 < t2) + self.assertTrue(t2 > t1) + self.assertTrue(t1 <= t2) + self.assertTrue(t2 >= t1) + self.assertTrue(t1 != t2) + self.assertTrue(t2 != t1) + self.assertTrue(not t1 == t2) + self.assertTrue(not t2 == t1) + self.assertTrue(not t1 > t2) + self.assertTrue(not t2 < t1) + self.assertTrue(not t1 >= t2) + self.assertTrue(not t2 <= t1) + + for badarg in OTHERSTUFF: + self.assertEqual(t1 == badarg, False) + self.assertEqual(t1 != badarg, True) + self.assertEqual(badarg == t1, False) + self.assertEqual(badarg != t1, True) + + self.assertRaises(TypeError, lambda: t1 <= badarg) + self.assertRaises(TypeError, lambda: t1 < badarg) + self.assertRaises(TypeError, lambda: t1 > badarg) + self.assertRaises(TypeError, lambda: t1 >= badarg) + self.assertRaises(TypeError, lambda: badarg <= t1) + self.assertRaises(TypeError, lambda: badarg < t1) + self.assertRaises(TypeError, lambda: badarg > t1) + self.assertRaises(TypeError, lambda: badarg >= t1) + + def test_bad_constructor_arguments(self): + # bad hours + self.theclass(0, 0) # no exception + self.theclass(23, 0) # no exception + self.assertRaises(ValueError, self.theclass, -1, 0) + self.assertRaises(ValueError, self.theclass, 24, 0) + # bad minutes + self.theclass(23, 0) # no exception + self.theclass(23, 59) # no exception + self.assertRaises(ValueError, self.theclass, 23, -1) + self.assertRaises(ValueError, self.theclass, 23, 60) + # bad seconds + self.theclass(23, 59, 0) # no exception + self.theclass(23, 59, 59) # no exception + self.assertRaises(ValueError, self.theclass, 23, 59, -1) + self.assertRaises(ValueError, self.theclass, 23, 59, 60) + # bad microseconds + self.theclass(23, 59, 59, 0) # no exception + self.theclass(23, 59, 59, 999999) # no exception + self.assertRaises(ValueError, self.theclass, 23, 59, 59, -1) + self.assertRaises(ValueError, self.theclass, 23, 59, 59, 1000000) + + def test_hash_equality(self): + d = self.theclass(23, 30, 17) + e = self.theclass(23, 30, 17) + self.assertEqual(d, e) + self.assertEqual(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + self.assertEqual(len(dic), 1) + self.assertEqual(dic[d], 2) + self.assertEqual(dic[e], 2) + + d = self.theclass(0, 5, 17) + e = self.theclass(0, 5, 17) + self.assertEqual(d, e) + self.assertEqual(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + self.assertEqual(len(dic), 1) + self.assertEqual(dic[d], 2) + self.assertEqual(dic[e], 2) + + def test_isoformat(self): + t = self.theclass(4, 5, 1, 123) + self.assertEqual(t.isoformat(), "04:05:01.000123") + self.assertEqual(t.isoformat(), str(t)) + + t = self.theclass() + self.assertEqual(t.isoformat(), "00:00:00") + self.assertEqual(t.isoformat(), str(t)) + + t = self.theclass(microsecond=1) + self.assertEqual(t.isoformat(), "00:00:00.000001") + self.assertEqual(t.isoformat(), str(t)) + + t = self.theclass(microsecond=10) + self.assertEqual(t.isoformat(), "00:00:00.000010") + self.assertEqual(t.isoformat(), str(t)) + + t = self.theclass(microsecond=100) + self.assertEqual(t.isoformat(), "00:00:00.000100") + self.assertEqual(t.isoformat(), str(t)) + + t = self.theclass(microsecond=1000) + self.assertEqual(t.isoformat(), "00:00:00.001000") + self.assertEqual(t.isoformat(), str(t)) + + t = self.theclass(microsecond=10000) + self.assertEqual(t.isoformat(), "00:00:00.010000") + self.assertEqual(t.isoformat(), str(t)) + + t = self.theclass(microsecond=100000) + self.assertEqual(t.isoformat(), "00:00:00.100000") + self.assertEqual(t.isoformat(), str(t)) + + def test_1653736(self): + # verify it doesn't accept extra keyword arguments + t = self.theclass(second=1) + self.assertRaises(TypeError, t.isoformat, foo=3) + + def test_strftime(self): + t = self.theclass(1, 2, 3, 4) + self.assertEqual(t.strftime("%H %M %S %f"), "01 02 03 000004") + # A naive object replaces %z and %Z with empty strings. + self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''") + # bpo-34482: Check that surrogates don't cause a crash. + try: + t.strftime("%H\ud800%M") + except UnicodeEncodeError: + pass + + def test_format(self): + t = self.theclass(1, 2, 3, 4) + self.assertEqual(t.__format__(""), str(t)) + + with self.assertRaisesRegex(TypeError, "must be str, not int"): + t.__format__(123) + + # check that a derived class's __str__() gets called + class A(self.theclass): + def __str__(self): + return "A" + + a = A(1, 2, 3, 4) + self.assertEqual(a.__format__(""), "A") + + # check that a derived class's strftime gets called + class B(self.theclass): + def strftime(self, format_spec): + return "B" + + b = B(1, 2, 3, 4) + self.assertEqual(b.__format__(""), str(t)) + + for fmt in [ + "%H %M %S", + ]: + self.assertEqual(t.__format__(fmt), t.strftime(fmt)) + self.assertEqual(a.__format__(fmt), t.strftime(fmt)) + self.assertEqual(b.__format__(fmt), "B") + + def test_str(self): + self.assertEqual(str(self.theclass(1, 2, 3, 4)), "01:02:03.000004") + self.assertEqual(str(self.theclass(10, 2, 3, 4000)), "10:02:03.004000") + self.assertEqual(str(self.theclass(0, 2, 3, 400000)), "00:02:03.400000") + self.assertEqual(str(self.theclass(12, 2, 3, 0)), "12:02:03") + self.assertEqual(str(self.theclass(23, 15, 0, 0)), "23:15:00") + + def test_repr(self): + name = "datetime." + self.theclass.__name__ + self.assertEqual(repr(self.theclass(1, 2, 3, 4)), "%s(1, 2, 3, 4)" % name) + self.assertEqual( + repr(self.theclass(10, 2, 3, 4000)), "%s(10, 2, 3, 4000)" % name + ) + self.assertEqual( + repr(self.theclass(0, 2, 3, 400000)), "%s(0, 2, 3, 400000)" % name + ) + self.assertEqual(repr(self.theclass(12, 2, 3, 0)), "%s(12, 2, 3)" % name) + self.assertEqual(repr(self.theclass(23, 15, 0, 0)), "%s(23, 15)" % name) + + @unittest.skip("Skip for CircuitPython - not implemented") + def test_resolution_info(self): + self.assertIsInstance(self.theclass.min, self.theclass) + self.assertIsInstance(self.theclass.max, self.theclass) + self.assertIsInstance(self.theclass.resolution, timedelta) + self.assertTrue(self.theclass.max > self.theclass.min) + + @unittest.skip("Skip for CircuitPython - not implemented") + def test_pickling(self): + args = 20, 59, 16, 64 ** 2 + orig = self.theclass(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + self.assertEqual(orig, derived) + + @unittest.skip("Skip for CircuitPython - not implemented") + def test_pickling_subclass_time(self): + args = 20, 59, 16, 64 ** 2 + orig = SubclassTime(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + self.assertEqual(orig, derived) + + def test_bool(self): + # time is always True. + cls = self.theclass + self.assertTrue(cls(1)) + self.assertTrue(cls(0, 1)) + self.assertTrue(cls(0, 0, 1)) + self.assertTrue(cls(0, 0, 0, 1)) + self.assertTrue(cls(0)) + self.assertTrue(cls()) + + @unittest.skip("Skip for CircuitPython - replace() not implemented") + def test_replace(self): + cls = self.theclass + args = [1, 2, 3, 4] + base = cls(*args) + self.assertEqual(base, base.replace()) + + i = 0 + for name, newval in ( + ("hour", 5), + ("minute", 6), + ("second", 7), + ("microsecond", 8), + ): + newargs = args[:] + newargs[i] = newval + expected = cls(*newargs) + got = base.replace(**{name: newval}) + self.assertEqual(expected, got) + i += 1 + + # Out of bounds. + base = cls(1) + self.assertRaises(ValueError, base.replace, hour=24) + self.assertRaises(ValueError, base.replace, minute=-1) + self.assertRaises(ValueError, base.replace, second=100) + self.assertRaises(ValueError, base.replace, microsecond=1000000) + + @unittest.skip("Skip for CircuitPython - replace() not implemented") + def test_subclass_replace(self): + class TimeSubclass(self.theclass): + pass + + ctime = TimeSubclass(12, 30) + self.assertIs(type(ctime.replace(hour=10)), TimeSubclass) + + def test_subclass_time(self): + class C(self.theclass): + theAnswer = 42 + + def __new__(cls, *args, **kws): + temp = kws.copy() + extra = temp.pop("extra") + result = self.theclass.__new__(cls, *args, **temp) + result.extra = extra + return result + + def newmeth(self, start): + return start + self.hour + self.second + + args = 4, 5, 6 + + dt1 = self.theclass(*args) + dt2 = C(*args, **{"extra": 7}) + + self.assertEqual(dt2.__class__, C) + self.assertEqual(dt2.theAnswer, 42) + self.assertEqual(dt2.extra, 7) + self.assertEqual(dt1.isoformat(), dt2.isoformat()) + self.assertEqual(dt2.newmeth(-7), dt1.hour + dt1.second - 7) + + def test_backdoor_resistance(self): + # see TestDate.test_backdoor_resistance(). + base = "2:59.0" + for hour_byte in " ", "9", chr(24), "\xff": + self.assertRaises(TypeError, self.theclass, hour_byte + base[1:]) + # Good bytes, but bad tzinfo: + with self.assertRaises(TypeError): + self.theclass(bytes([1] * len(base)), "EST")