From 0fd2c1f44e5ea51438a9eea5393d5f109ee8014d Mon Sep 17 00:00:00 2001 From: Juergen Edelbluth Date: Sun, 14 Aug 2022 20:01:28 +0000 Subject: [PATCH] Initial Commit --- .gitignore | 234 ++++++ .idea/.gitignore | 8 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/pytest-csv-params.iml | 14 + .idea/vcs.xml | 6 + .pylintrc | 611 ++++++++++++++++ .python-version | 4 + LICENSE.txt | 20 + README.md | 132 ++++ _ptcsvp/__init__.py | 0 _ptcsvp/configure.py | 19 + _ptcsvp/parametrize.py | 101 +++ _ptcsvp/plugin.py | 15 + _ptcsvp/version.py | 30 + poetry.lock | 690 ++++++++++++++++++ pyproject.toml | 132 ++++ pytest_csv_params/__init__.py | 0 pytest_csv_params/decorator.py | 6 + pytest_csv_params/dialect.py | 18 + pytest_csv_params/exception.py | 21 + pytest_csv_params/plugin.py | 15 + pytest_csv_params/types.py | 17 + tests/__init__.py | 0 tests/assets/test1.csv | 6 + tests/assets/test10.csv | 10 + tests/assets/test11.csv | 6 + tests/assets/test2.csv | 6 + tests/assets/test3.csv | 6 + tests/assets/test4.csv | 6 + tests/assets/test5.csv | 6 + tests/assets/test6.csv | 6 + tests/assets/test7.csv | 1 + tests/assets/test8.csv | 0 tests/assets/test9.csv | 4 + tests/conftest.py | 5 + tests/plugin/__init__.py | 0 tests/plugin/assets/.gitattributes | 1 + tests/plugin/assets/bad-test.csv | 4 + tests/plugin/assets/fruit_test.tpl | 13 + tests/plugin/assets/simple-test.csv | 5 + tests/plugin/conftest.py | 56 ++ tests/plugin/test_plugin.py | 29 + tests/test_parametrize.py | 97 +++ tests/test_read_csv.py | 48 ++ tests/test_version_check.py | 118 +++ 47 files changed, 2544 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/pytest-csv-params.iml create mode 100644 .idea/vcs.xml create mode 100644 .pylintrc create mode 100644 .python-version create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 _ptcsvp/__init__.py create mode 100644 _ptcsvp/configure.py create mode 100644 _ptcsvp/parametrize.py create mode 100644 _ptcsvp/plugin.py create mode 100644 _ptcsvp/version.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 pytest_csv_params/__init__.py create mode 100644 pytest_csv_params/decorator.py create mode 100644 pytest_csv_params/dialect.py create mode 100644 pytest_csv_params/exception.py create mode 100644 pytest_csv_params/plugin.py create mode 100644 pytest_csv_params/types.py create mode 100644 tests/__init__.py create mode 100644 tests/assets/test1.csv create mode 100644 tests/assets/test10.csv create mode 100644 tests/assets/test11.csv create mode 100644 tests/assets/test2.csv create mode 100644 tests/assets/test3.csv create mode 100644 tests/assets/test4.csv create mode 100644 tests/assets/test5.csv create mode 100644 tests/assets/test6.csv create mode 100644 tests/assets/test7.csv create mode 100644 tests/assets/test8.csv create mode 100644 tests/assets/test9.csv create mode 100644 tests/conftest.py create mode 100644 tests/plugin/__init__.py create mode 100644 tests/plugin/assets/.gitattributes create mode 100644 tests/plugin/assets/bad-test.csv create mode 100644 tests/plugin/assets/fruit_test.tpl create mode 100644 tests/plugin/assets/simple-test.csv create mode 100644 tests/plugin/conftest.py create mode 100644 tests/plugin/test_plugin.py create mode 100644 tests/test_parametrize.py create mode 100644 tests/test_read_csv.py create mode 100644 tests/test_version_check.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2a4a48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,234 @@ +# Test Reports +test-reports/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + + Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7981ae6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f4ab084 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/pytest-csv-params.iml b/.idea/pytest-csv-params.iml new file mode 100644 index 0000000..7a6134d --- /dev/null +++ b/.idea/pytest-csv-params.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..14edb76 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,611 @@ +[MAIN] + +# 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 + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# 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-allow-list= + +# 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. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS .git + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. The default value ignores Emacs file +# locks +ignore-patterns=^\.# + +# 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= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.8 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 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={path}:{line}:{column}: {msg_id}: {msg} ({symbol}) + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# 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 re-enable 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=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + couldnt-parse + +# 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=c-extension-no-member + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=new + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --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 + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[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 to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=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 symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# 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,argparse.Namespace + +# 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 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# 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=cls + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# 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. expected to +# not be 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,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# 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=120 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# 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 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to 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 + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=BaseException, + Exception + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=10 + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +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=20 + +# 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=2 + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# 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. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..5965a51 --- /dev/null +++ b/.python-version @@ -0,0 +1,4 @@ +3.10.6 +3.9.13 +3.8.13 +3.11-dev diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..477fcc3 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright Juergen Edelbluth and other contributors, https://juergen.rocks/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8b63fb --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# pytest-csv-params + +A pytest plugin to parametrize tests by CSV files. + +## Requirements + +- Python 3.8, 3.9 or 3.10 +- pytest >= 7.1 + +There's no operating system dependend code in this plugin, so it should run anywhere where pytest runs. + +## Installation + +Simply install it with pip... + +```bash +pip install pytest-csv-plugin +``` + +... or poetry ... + +```bash +poetry add --dev pytest-csv-plugin +``` + +## Usage + +Simply decorate your test method with `@csv_params` and the following parameters: + +| Parameter | Type | Description | Example | +|--------------|--------------------------|-----------------------------------------------------------|-------------------------------------| +| `data_file` | `str` | The CSV file to use, relative or absolute path | `/var/testdata/test1.csv` | +| `base_dir` | `str` (optional) | Directory to look up relative CSV files (see `data_file`) | `join(dirname(__file__), "assets")` | +| `id_col` | `str` (optional) | Column name of the CSV that contains test case IDs | `ID#` | +| `dialect` | `csv.Dialect` (optional) | CSV Dialect definition (see [1]) | `csv.excel_tab` | +| `data_casts` | `dict` (optional) | Cast Methods for the CSV Data (see "Data Casting" below) | `{ "a": int, "b": float }` | + +[1] [Python CSV Documentation](https://docs.python.org/3/library/csv.html#dialects-and-formatting-parameters) + +### Data Casting + +When data is read from CSV, they are always parsed as `str`. If you need them in other formats, you can set a method that should be called with the value. + +These methods can also be lambdas, and are also good for further transformations. + +#### Data Casting Example + +```python +from pytest_csv_params.decorator import csv_params + +def normalize(x: str) -> str: + return x.strip().upper() + +@csv_params( + data_file="/test/data.csv", + data_casts={ + "col_x": normalize, + "col_y": float, + }, +) +def test_something(col_x, col_y): + # Test something... + ... +``` + +### CSV Format + +The default CSV format is: + +- `\r\n` as line ending +- All non-numeric fields are surrounded by " +- If you need a " in the value, use "" (double quote) +- Fields are separated by comma (",") + +#### Example CSV + +```text +"ID#", "part_a", "part_b", "expected_result" +"first", 1, 2, 3 +"second", 3, 4, 7 +"third", 10, 11, 21 +``` + +### Usage Example + +This example uses the CSV example from above. + +```python +from pytest_csv_params.decorator import csv_params + +@csv_params( + data_file="/data/test-lib/cases/addition.csv", + id_col="#ID", + data_casts={ + "part_a": int, + "part_b": int, + "expected_result": int, + }, +) +def test_addition(part_a, part_b, expected_result): + assert part_a + part_b == expected_result +``` + +## Contributing + +### Build and test + +You need [Poetry](https://python-poetry.org/) in order to build this project. + +Tests are implemented with `pytest`, and `tox` is used to orchestrate them for the supported python versions. + +- Checkout this repo +- Run `poetry install` +- Run `poetry run tox` (for all supported python versions) or `poetry run pytest` (for your current version) + +### Bugs etc. + +Please send your issues to `csv-params_issues` (at) `jued.de`. Please include the following: + +- Plugin Version used +- Pytest version +- Python version with operating system + +It would be great if you could include example code that clarifies your issue. + +### Pull Requests + +Pull requests are always welcome. Since this Gitea instance is not open to public, just send an e-mail to discuss options. + +## License + +Code is under MIT license. See [LICENCE.txt](./LICENSE.txt) for details. diff --git a/_ptcsvp/__init__.py b/_ptcsvp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/_ptcsvp/configure.py b/_ptcsvp/configure.py new file mode 100644 index 0000000..4b44f7f --- /dev/null +++ b/_ptcsvp/configure.py @@ -0,0 +1,19 @@ +""" +Pytest Plugin Configuration +""" + +from _ptcsvp.plugin import Plugin + + +def pytest_configure(config, plugin_name="csv_params"): + """ + Register our Plugin + """ + config.pluginmanager.register(Plugin(config), f"{plugin_name}_plugin") + + +def pytest_unconfigure(config, plugin_name="csv_params"): + """ + Remove our Plugin + """ + config.pluginmanager.unregister(f"{plugin_name}_plugin") diff --git a/_ptcsvp/parametrize.py b/_ptcsvp/parametrize.py new file mode 100644 index 0000000..25e11ac --- /dev/null +++ b/_ptcsvp/parametrize.py @@ -0,0 +1,101 @@ +""" +Parametrize a test function by CSV file +""" +import csv +from pathlib import Path +from typing import List, Optional, TypedDict + +import pytest + +from pytest_csv_params.dialect import CsvParamsDefaultDialect +from pytest_csv_params.exception import ( + CsvParamsDataFileInaccessible, + CsvParamsDataFileInvalid, + CsvParamsDataFileNotFound, +) +from pytest_csv_params.types import BaseDir, CsvDialect, DataCastDict, DataFile, IdColName + + +class TestCaseParameters(TypedDict): + """ + Type for Test Case + """ + + test_id: Optional[str] + data: List + + +def read_csv(base_dir: BaseDir, data_file: DataFile, dialect: CsvDialect): + """ + Get Data from CSV + """ + + if data_file is None: + raise CsvParamsDataFileInvalid("Data file is None") from None + csv_file = Path(data_file) + if not csv_file.is_absolute(): + if base_dir is not None: + csv_file = Path(base_dir) / csv_file + if not csv_file.exists() or not csv_file.is_file(): + raise CsvParamsDataFileNotFound(f"Cannot find file: {str(csv_file)}") from None + csv_lines = [] + try: + with open(csv_file, newline="", encoding="utf-8") as csv_file_handle: + csv_reader = csv.reader(csv_file_handle, dialect=dialect) + for row in csv_reader: + csv_lines.append(row) + except IOError as err: # pragma: no cover + raise CsvParamsDataFileInaccessible(f"Unable to read file: {str(csv_file)}") from err + except csv.Error as err: + raise CsvParamsDataFileInvalid("Invalid data") from err + return csv_lines + + +def add_parametrization( + base_dir: BaseDir = None, + data_file: DataFile = None, + id_col: IdColName = None, + data_casts: DataCastDict = None, + dialect: CsvDialect = CsvParamsDefaultDialect, +): + """ + Get data from the files and add things to the tests + """ + csv_lines = read_csv(base_dir, data_file, dialect) + if len(csv_lines) < 2: + raise CsvParamsDataFileInvalid("File does not contain a single data row") from None + id_index = -1 + headers = csv_lines.pop(0) + if id_col is not None: + try: + id_index = headers.index(id_col) + del headers[id_index] + except ValueError as err: + raise CsvParamsDataFileInvalid(f"Cannot find ID column '{id_col}'") from err + if len(headers) == 0: + raise CsvParamsDataFileInvalid("File seems only to have IDs") from None + data: List[TestCaseParameters] = [] + for data_line in csv_lines: + line = list(map(str, data_line)) + if len(line) == 0: + continue + test_id = None + if id_index >= 0: + test_id = line.pop(id_index) + if len(headers) != len(line): + raise CsvParamsDataFileInvalid("Header and Data length mismatch") from None + if data_casts is not None: + for index, header in enumerate(headers): + caster = data_casts.get(header, None) + if caster is not None: + line[index] = caster(str(line[index])) + data.append( + { + "test_id": test_id, + "data": line, + } + ) + id_data = None + if id_col is not None: + id_data = [tc_data["test_id"] for tc_data in data] + return pytest.mark.parametrize(headers, [tc_data["data"] for tc_data in data], ids=id_data) diff --git a/_ptcsvp/plugin.py b/_ptcsvp/plugin.py new file mode 100644 index 0000000..064b1bc --- /dev/null +++ b/_ptcsvp/plugin.py @@ -0,0 +1,15 @@ +""" +The main Plugin implementation +""" + + +class Plugin: # pylint: disable=too-few-public-methods + """ + Plugin Class + """ + + def __init__(self, config): + """ + Hold the pytest config + """ + self.config = config diff --git a/_ptcsvp/version.py b/_ptcsvp/version.py new file mode 100644 index 0000000..45295f3 --- /dev/null +++ b/_ptcsvp/version.py @@ -0,0 +1,30 @@ +""" +Check Version Information +""" +import sys + +from attr.exceptions import PythonTooOldError +from packaging.version import parse + + +def check_python_version(min_version=(3, 8)): + """ + Check if the current version is at least 3.8 + """ + + if sys.version_info < min_version: + raise PythonTooOldError(f"At least Python {'.'.join(map(str, min_version))} required") + + +def check_pytest_version(min_version=(7, 1)): + """ + Check if the current version is at least 7.1 + """ + + from pytest import __version__ as pytest_version # pylint: disable=import-outside-toplevel + + pytest_min_version = ".".join(map(str, min_version)) + parsed_min_version = parse(pytest_min_version) + parsed_actual_version = parse(pytest_version) + if parsed_actual_version < parsed_min_version: + raise RuntimeError(f"At least Pytest {pytest_min_version} required") diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..01e13e8 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,690 @@ +[[package]] +name = "astroid" +version = "2.11.7" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = ">=1.11,<2" + +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "bandit" +version = "1.7.4" +description = "Security oriented static analyser for python code." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +GitPython = ">=1.0.1" +PyYAML = ">=5.3.1" +stevedore = ">=1.20.0" + +[package.extras] +test = ["coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "toml", "beautifulsoup4 (>=4.8.0)", "pylint (==1.9.4)"] +toml = ["toml"] +yaml = ["pyyaml"] + +[[package]] +name = "black" +version = "22.6.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "coverage" +version = "6.4.3" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "dill" +version = "0.3.5.1" +description = "serialize all of python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "distlib" +version = "0.3.5" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "filelock" +version = "3.8.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +testing = ["pytest-timeout (>=2.1)", "pytest-cov (>=3)", "pytest (>=7.1.2)", "coverage (>=6.4.2)", "covdefaults (>=2.2)"] +docs = ["sphinx-autodoc-typehints (>=1.19.1)", "sphinx (>=5.1.1)", "furo (>=2022.6.21)"] + +[[package]] +name = "gitdb" +version = "4.0.9" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.27" +description = "GitPython is a python library used to interact with Git repositories" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "lazy-object-proxy" +version = "1.7.1" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "pbr" +version = "5.10.0" +description = "Python Build Reasonableness" +category = "dev" +optional = false +python-versions = ">=2.6" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pprintpp" +version = "0.4.0" +description = "A drop-in replacement for pprint that's actually pretty" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pygments" +version = "2.12.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pylint" +version = "2.14.5" +description = "python code static checker" +category = "dev" +optional = false +python-versions = ">=3.7.2" + +[package.dependencies] +astroid = ">=2.11.6,<=2.12.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = ">=0.2" +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-bandit" +version = "0.6.1" +description = "A bandit plugin for pytest" +category = "dev" +optional = false +python-versions = "~=3.4" + +[package.dependencies] +bandit = ">=1.4.0" +pytest = ">=3.5.0" + +[[package]] +name = "pytest-black" +version = "0.3.12" +description = "A pytest plugin to enable format checking with black" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +black = {version = "*", markers = "python_version >= \"3.6\""} +pytest = ">=3.5.0" +toml = "*" + +[[package]] +name = "pytest-clarity" +version = "1.0.1" +description = "A plugin providing an alternative, colourful diff output for failing assertions." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pprintpp = ">=0.4.0" +pytest = ">=3.5.0" +rich = ">=8.0.0" + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-isort" +version = "3.0.0" +description = "py.test plugin to check import ordering using isort" +category = "dev" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +isort = ">=4.0" +pytest = ">=5.0" + +[[package]] +name = "pytest-mock" +version = "3.8.2" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pytest-asyncio", "tox", "pre-commit"] + +[[package]] +name = "pytest-pylint" +version = "0.18.0" +description = "pytest plugin to check source code with pylint" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +pylint = ">=2.3.0" +pytest = ">=5.4" +toml = ">=0.7.1" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "rich" +version = "12.5.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "dev" +optional = false +python-versions = ">=3.6.3,<4.0.0" + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "stevedore" +version = "4.0.0" +description = "Manage dynamic plugins for Python applications" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tomlkit" +version = "0.11.4" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[[package]] +name = "tox" +version = "3.25.1" +description = "tox is a generic virtualenv management and test command line tool" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} +filelock = ">=3.0.0" +packaging = ">=14" +pluggy = ">=0.12.0" +py = ">=1.4.17" +six = ">=1.14.0" +toml = ">=0.9.4" +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" + +[package.extras] +testing = ["pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest-randomly (>=1.0.0)", "pytest-mock (>=1.10.0)", "pytest-cov (>=2.5.1)", "pytest (>=4.0.0)", "freezegun (>=0.3.11)", "flaky (>=3.4.0)"] +docs = ["towncrier (>=18.5.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "sphinx (>=2.0.0)", "pygments-github-lexers (>=0.0.5)"] + +[[package]] +name = "tox-poetry" +version = "0.4.1" +description = "Tox poetry plugin" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pluggy = "*" +toml = "*" +tox = {version = ">=3.7.0", markers = "python_version >= \"3\""} + +[package.extras] +test = ["pylint", "pycodestyle", "pytest", "coverage"] + +[[package]] +name = "typing-extensions" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "virtualenv" +version = "20.16.3" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +distlib = ">=0.3.5,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<3" + +[package.extras] +testing = ["pytest-timeout (>=2.1)", "pytest-randomly (>=3.10.3)", "pytest-mock (>=3.6.1)", "pytest-freezegun (>=0.4.2)", "pytest-env (>=0.6.2)", "pytest (>=7.0.1)", "packaging (>=21.3)", "flaky (>=3.7)", "coverage-enable-subprocess (>=1)", "coverage (>=6.2)"] +docs = ["towncrier (>=21.9)", "sphinx-rtd-theme (>=1)", "sphinx-argparse (>=0.3.1)", "sphinx (>=5.1.1)", "proselint (>=0.13)"] + +[[package]] +name = "wrapt" +version = "1.14.1" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "109ba73e69f54a4f4610c2cc9153499d8d8fb6db5dffc48ab767779e4525be78" + +[metadata.files] +astroid = [] +atomicwrites = [] +attrs = [] +bandit = [] +black = [] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] +coverage = [] +dill = [] +distlib = [] +filelock = [] +gitdb = [] +gitpython = [] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [] +lazy-object-proxy = [] +mccabe = [] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [] +pbr = [] +platformdirs = [] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +pprintpp = [ + {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"}, + {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pygments = [ + {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, + {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, +] +pylint = [] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] +pytest-bandit = [] +pytest-black = [] +pytest-clarity = [ + {file = "pytest-clarity-1.0.1.tar.gz", hash = "sha256:505fe345fad4fe11c6a4187fe683f2c7c52c077caa1e135f3e483fe112db7772"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +pytest-isort = [] +pytest-mock = [] +pytest-pylint = [] +pyyaml = [] +rich = [] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +smmap = [] +stevedore = [] +toml = [] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +tomlkit = [] +tox = [] +tox-poetry = [] +typing-extensions = [] +virtualenv = [] +wrapt = [] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..02d4fe9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,132 @@ +[tool.poetry] +name = "pytest-csv-params" +version = "0.0.1" +description = "Pytest plugin for Test Case Parametrization with CSV files" +authors = ["Juergen Edelbluth "] +license = "MIT" +repository = "https://git.codebau.dev/pytest-plugins/pytest-csv-params" +homepage = "https://git.codebau.dev/pytest-plugins/pytest-csv-params" +keywords = [ + "py.test", "pytest", "csv", "params", "parametrize", "pytest-plugin", +] +classifiers = [ + "Development Status :: 1 - Planning", + "Environment :: Plugins", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Quality Assurance", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X", + "Topic :: Utilities", +] +packages = [ + { include = "_ptcsvp" }, + { include = "pytest_csv_params" }, +] + +[tool.poetry.urls] +"Issue Tracker" = "https://git.codebau.dev/pytest-plugins/pytest-csv-params/issues" +"Wiki" = "https://git.codebau.dev/pytest-plugins/pytest-csv-params/wiki" +"Releases" = "https://git.codebau.dev/pytest-plugins/pytest-csv-params/releases" + +[tool.poetry.plugins."pytest11"] +"pytest-csv-params" = "pytest_csv_params.plugin" + +[tool.pytest.ini_options] +addopts = "--black --isort --pylint --pylint-rcfile=.pylintrc --cov --cov-report=term-missing --junitxml=test-reports/pytest_csv_params.xml" +filterwarnings=[ + "ignore:.*BlackItem.*:_pytest.warning_types.PytestDeprecationWarning", + "ignore:.*BlackItem.*:_pytest.warning_types.PytestRemovedIn8Warning", + "ignore:.*PyLintFile.*:_pytest.warning_types.PytestRemovedIn8Warning", + "ignore:.*BlackItem.*:_pytest.warning_types.PytestWarning", + "ignore:Using the __implements__ inheritance pattern for BaseReporter is no longer supported. Child classes should only inherit BaseReporter", + "ignore:.*zipimport.*", + "ignore:Creating a LegacyVersion has been deprecated and will be removed in the next major release", +] +junit_family = "xunit2" +junit_logging = "all" +junit_log_passing_tests = true +junit_duration_report = "call" +junit_suite_name = "pytest-csv-params" + +bandit_targets = [ + "pytest_csv_params", + "_ptcsvp", +] +bandit_recurse = "True" + +[tool.coverage] + +[tool.coverage.run] +omit = [ + "test/*", + "tests/*", + "*/venv/*", + "*/.venv/*", + "*/.tox/*", + "*/pyvenv/*", + "*/JetBrains/*", + "*/virtualenv/*", + "/etc/python*", + "*pip-standalone-pip*", + "*pip-build-env*", + "*/test_plugin_test_multiplication.py", + "*/test_plugin_test_error.py", +] +relative_files = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self\\.debug", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", +] + +[tool.tox] +legacy_tox_ini = """ +[tox] +minversion = 3.25.0 +envlist = py38,py39,py310 + +[testenv] +commands = + pytest +""" + +[tool.black] +line-length = 120 +target-version = ['py38', 'py39', 'py310'] +include = '\.pyi?$' + +[tool.isort] +line_length = 120 +include_trailing_comma = "True" +multi_line_output = 3 + +[tool.poetry.dependencies] +python = "^3.8" +pytest = "^7.1.2" + +[tool.poetry.dev-dependencies] +tox = "^3.25.1" +tox-poetry = "^0.4.1" +pytest-black = "^0.3.12" +pytest-isort = "^3.0.0" +pytest-cov = "^3.0.0" +pytest-pylint = "^0.18.0" +pytest-mock = "^3.8.2" +pytest-clarity = "^1.0.1" +pytest-bandit = "^0.6.1" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/pytest_csv_params/__init__.py b/pytest_csv_params/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pytest_csv_params/decorator.py b/pytest_csv_params/decorator.py new file mode 100644 index 0000000..96bc27d --- /dev/null +++ b/pytest_csv_params/decorator.py @@ -0,0 +1,6 @@ +""" +Add CSV Data to test +""" +from _ptcsvp.parametrize import add_parametrization + +csv_params = add_parametrization diff --git a/pytest_csv_params/dialect.py b/pytest_csv_params/dialect.py new file mode 100644 index 0000000..7fd9502 --- /dev/null +++ b/pytest_csv_params/dialect.py @@ -0,0 +1,18 @@ +""" +CSV Dialects +""" +import csv + + +class CsvParamsDefaultDialect(csv.Dialect): # pylint: disable=too-few-public-methods + """ + Basic CSV Dialect for most Tests + """ + + delimiter = "," + doublequote = True + lineterminator = "\r\n" + quotechar = '"' + quoting = csv.QUOTE_ALL + strict = True + skipinitialspace = True diff --git a/pytest_csv_params/exception.py b/pytest_csv_params/exception.py new file mode 100644 index 0000000..08505be --- /dev/null +++ b/pytest_csv_params/exception.py @@ -0,0 +1,21 @@ +""" +Exceptions +""" + + +class CsvParamsDataFileNotFound(FileNotFoundError): + """ + File Not Found + """ + + +class CsvParamsDataFileInaccessible(IOError): + """ + Cannot Access the File + """ + + +class CsvParamsDataFileInvalid(ValueError): + """ + CSV Data is somehow invalid + """ diff --git a/pytest_csv_params/plugin.py b/pytest_csv_params/plugin.py new file mode 100644 index 0000000..6e75efd --- /dev/null +++ b/pytest_csv_params/plugin.py @@ -0,0 +1,15 @@ +""" +Pytest Plugin Entrypoint +""" + +from _ptcsvp.configure import pytest_configure as _pytest_configure +from _ptcsvp.configure import pytest_unconfigure as _pytest_unconfigure +from _ptcsvp.version import check_pytest_version, check_python_version + +# Fist at all, check if the python & pytest version matches +check_python_version() +check_pytest_version() + +# Basic config +pytest_configure = _pytest_configure +pytest_unconfigure = _pytest_unconfigure diff --git a/pytest_csv_params/types.py b/pytest_csv_params/types.py new file mode 100644 index 0000000..e6a5d31 --- /dev/null +++ b/pytest_csv_params/types.py @@ -0,0 +1,17 @@ +""" +Types to ease the usage of the API +""" +import csv +from typing import Callable, Dict, Optional, Type, TypeVar + +T = TypeVar("T") + +DataCast = Callable[[str], T] +DataCastDict = Dict[str, DataCast] + +BaseDir = Optional[str] +IdColName = Optional[str] + +DataFile = str + +CsvDialect = Type[csv.Dialect] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/assets/test1.csv b/tests/assets/test1.csv new file mode 100644 index 0000000..1320b07 --- /dev/null +++ b/tests/assets/test1.csv @@ -0,0 +1,6 @@ +"#ID", "Animal", "Color" +"A1", "Cat", "Blue" +"A2", "Dog", "Green" +"A3", "Mouse", "Yellow" +"A4", "Bird", "Purple" +"A5", "Cow", "Red" diff --git a/tests/assets/test10.csv b/tests/assets/test10.csv new file mode 100644 index 0000000..7279487 --- /dev/null +++ b/tests/assets/test10.csv @@ -0,0 +1,10 @@ +"#ID", "Animal", "Color" +"A1", "Cat", "Blue" +"A2", "Dog", "Green" +"A3", "Mouse", "Yellow" +"A4", "Bird", "Purple" +"A5", "Cow", "Red" + + + +"A7", "Rat", "Black" diff --git a/tests/assets/test11.csv b/tests/assets/test11.csv new file mode 100644 index 0000000..c830240 --- /dev/null +++ b/tests/assets/test11.csv @@ -0,0 +1,6 @@ +"Animal", "Color" +"Cat", "Blue" +"Dog", "Green" +"Mouse", "Yellow" +"Bird", "Purple" +"Cow", "Red" diff --git a/tests/assets/test2.csv b/tests/assets/test2.csv new file mode 100644 index 0000000..f506dad --- /dev/null +++ b/tests/assets/test2.csv @@ -0,0 +1,6 @@ +"#ID", "Animal", "Color" +"A1", "Cat", "Blue" +"A2", "Dog", "Green" +"A3", "Mouse", "Yellow" +"A4", "Bird", "Purple" +"A5", "Cow", "Red", "46" diff --git a/tests/assets/test3.csv b/tests/assets/test3.csv new file mode 100644 index 0000000..6517742 --- /dev/null +++ b/tests/assets/test3.csv @@ -0,0 +1,6 @@ +"#ID", "Animal", "Color", "Number" +"A1", "Cat", "Blue", "44" +"A2", "Dog", "Green", "45" +"A3", "Mouse", "Yellow" +"A4", "Bird", "Purple" +"A5", "Cow", "Red", "46" diff --git a/tests/assets/test4.csv b/tests/assets/test4.csv new file mode 100644 index 0000000..b849ef5 --- /dev/null +++ b/tests/assets/test4.csv @@ -0,0 +1,6 @@ +"#ID", "Animal", "Color", "Number" +"A1", "Cat", "Blue", "44" +"A2", "Dog", "Green", "45" +"A3", "Mouse", "Yellow" +"A4", "Bird", "Purple +"A5", "Cow", "Red", "46" diff --git a/tests/assets/test5.csv b/tests/assets/test5.csv new file mode 100644 index 0000000..d1a401f --- /dev/null +++ b/tests/assets/test5.csv @@ -0,0 +1,6 @@ +"#ID", "Animal", "Color", "Number" +"A1", "Cat", "Blue", "44" +"A2", "Dog", "Green", "45" +"A3", "Mouse", "Yellow" +"A4", "Bird", "Purple" +"A5", "Cow", "Red", 46 diff --git a/tests/assets/test6.csv b/tests/assets/test6.csv new file mode 100644 index 0000000..f710b1f --- /dev/null +++ b/tests/assets/test6.csv @@ -0,0 +1,6 @@ +"#ID###", "Animal", "Color", "Number" +"A1", "Cat", "Blue", "44" +"A2", "Dog", "Green", "45" +"A3", "Mouse", "Yellow" +"A4", "Bird", "Purple" +"A5", "Cow", "Red", 46 diff --git a/tests/assets/test7.csv b/tests/assets/test7.csv new file mode 100644 index 0000000..00a7f45 --- /dev/null +++ b/tests/assets/test7.csv @@ -0,0 +1 @@ +"#ID", "Animal", "Color", "Number" diff --git a/tests/assets/test8.csv b/tests/assets/test8.csv new file mode 100644 index 0000000..e69de29 diff --git a/tests/assets/test9.csv b/tests/assets/test9.csv new file mode 100644 index 0000000..c2d454b --- /dev/null +++ b/tests/assets/test9.csv @@ -0,0 +1,4 @@ +"#ID" +"A1" +"A2" +"A3" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2fe5a44 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,5 @@ +""" +Conftest project global +""" + +pytest_plugins = ["pytester"] diff --git a/tests/plugin/__init__.py b/tests/plugin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/plugin/assets/.gitattributes b/tests/plugin/assets/.gitattributes new file mode 100644 index 0000000..c3230d5 --- /dev/null +++ b/tests/plugin/assets/.gitattributes @@ -0,0 +1 @@ +*.csv text eol=crlf diff --git a/tests/plugin/assets/bad-test.csv b/tests/plugin/assets/bad-test.csv new file mode 100644 index 0000000..81b61c6 --- /dev/null +++ b/tests/plugin/assets/bad-test.csv @@ -0,0 +1,4 @@ +"#ID","word","expected_length" +"Apple_length_5","Apple" +"Banana_length_6","Banana",6 +"Kiwi_length_4","Kiwi",4 diff --git a/tests/plugin/assets/fruit_test.tpl b/tests/plugin/assets/fruit_test.tpl new file mode 100644 index 0000000..932e707 --- /dev/null +++ b/tests/plugin/assets/fruit_test.tpl @@ -0,0 +1,13 @@ +import pytest +from pytest_csv_params.decorator import csv_params + + +@csv_params( + id_col="#ID", + data_file=r"{{data_file}}", + data_casts={ + "expected_length": int, + } +) +def test_fruits(word, expected_length): + assert len(word) == expected_length diff --git a/tests/plugin/assets/simple-test.csv b/tests/plugin/assets/simple-test.csv new file mode 100644 index 0000000..e322c09 --- /dev/null +++ b/tests/plugin/assets/simple-test.csv @@ -0,0 +1,5 @@ +"#ID","word","expected_length" +"Apple_length_5","Apple",5 +"Banana_length_6","Banana",6 +"Kiwi_length_4","Kiwi",4 +"Peach_wrong_length_10","Peach",10 diff --git a/tests/plugin/conftest.py b/tests/plugin/conftest.py new file mode 100644 index 0000000..5190345 --- /dev/null +++ b/tests/plugin/conftest.py @@ -0,0 +1,56 @@ +""" +Configuration for the tests +... and local Plugins +""" +import subprocess +from os.path import dirname, join + +import pytest + + +def get_csv(csv: str): + """ + Get CSV data + """ + with open(join(dirname(__file__), "assets", f"{csv}.csv"), "rb") as csv_fh: + csv_data = csv_fh.read() + return csv_data.decode("utf-8") + + +@pytest.fixture(scope="session") +def simple_test_csv(): + """ + Provide simple CSV data + """ + return get_csv("simple-test") + + +@pytest.fixture(scope="session") +def bad_test_csv(): + """ + Provide bad CSV data + """ + return get_csv("bad-test") + + +@pytest.fixture(scope="session") +def simple_fruit_test(): + """ + Provide simple test case + """ + with open(join(dirname(__file__), "assets", "fruit_test.tpl"), "rt", encoding="utf-8") as test_fh: + test_data = test_fh.read() + return lambda file: test_data.replace("{{data_file}}", file) + + +@pytest.fixture(scope="session", autouse=True) +def install_plugin_locally(pytestconfig): + """ + Install the local package + """ + root = pytestconfig.rootpath + _ = subprocess.run(["pip", "install", "-e", "."], shell=True, cwd=root, check=True, capture_output=True) + yield + _ = subprocess.run( + ["pip", "uninstall", "-y", "pytest_csv_params"], shell=True, cwd=root, check=True, capture_output=True + ) diff --git a/tests/plugin/test_plugin.py b/tests/plugin/test_plugin.py new file mode 100644 index 0000000..9180c09 --- /dev/null +++ b/tests/plugin/test_plugin.py @@ -0,0 +1,29 @@ +""" +Just try to call our plugin +""" + + +def test_plugin_test_multiplication(pytester, simple_test_csv, simple_fruit_test): + """ + Simple Roundtrip Smoke Test + """ + + csv_file = str(pytester.makefile(".csv", simple_test_csv).absolute()) + + pytester.makepyfile(simple_fruit_test(csv_file)) + + result = pytester.runpytest("-p", "no:bandit") + result.assert_outcomes(passed=3, failed=1) + + +def test_plugin_test_error(pytester, bad_test_csv, simple_fruit_test): + """ + Simple Error Behaviour Test + """ + + csv_file = str(pytester.makefile(".csv", bad_test_csv).absolute()) + + pytester.makepyfile(simple_fruit_test(csv_file)) + + result = pytester.runpytest("-p", "no:bandit") + result.assert_outcomes(errors=1) diff --git a/tests/test_parametrize.py b/tests/test_parametrize.py new file mode 100644 index 0000000..4d4f985 --- /dev/null +++ b/tests/test_parametrize.py @@ -0,0 +1,97 @@ +""" +Test the Parametrization Feature +""" +from os.path import dirname, join + +import pytest + +from _ptcsvp.parametrize import add_parametrization +from pytest_csv_params.exception import CsvParamsDataFileInvalid, CsvParamsDataFileNotFound + + +@pytest.mark.parametrize( + ["csv_file", "id_col", "result", "ids", "expect_exception", "expect_message"], + [ + ("test0", "#ID", None, None, CsvParamsDataFileNotFound, None), + ( + "test1", + "#ID", + ( + ["Animal", "Color"], + [ + ["Cat", "Blue"], + ["Dog", "Green"], + ["Mouse", "Yellow"], + ["Bird", "Purple"], + ["Cow", "Red"], + ], + ), + ["A1", "A2", "A3", "A4", "A5"], + None, + None, + ), + ("test2", "#ID", None, None, CsvParamsDataFileInvalid, "Header and Data length mismatch"), + ("test3", "#ID", None, None, CsvParamsDataFileInvalid, "Header and Data length mismatch"), + ("test4", "#ID", None, None, CsvParamsDataFileInvalid, "Invalid data"), + ("test5", "#ID", None, None, CsvParamsDataFileInvalid, "Header and Data length mismatch"), + ("test6", "#ID", None, None, CsvParamsDataFileInvalid, "Cannot find ID column '#ID'"), + ("test7", "#ID", None, None, CsvParamsDataFileInvalid, "File does not contain a single data row"), + ("test8", "#ID", None, None, CsvParamsDataFileInvalid, "File does not contain a single data row"), + ("test9", "#ID", None, None, CsvParamsDataFileInvalid, "File seems only to have IDs"), + ("test0", "#ID", None, None, CsvParamsDataFileNotFound, None), + ( + "test10", + "#ID", + ( + ["Animal", "Color"], + [ + ["Cat", "Blue"], + ["Dog", "Green"], + ["Mouse", "Yellow"], + ["Bird", "Purple"], + ["Cow", "Red"], + ["Rat", "Black"], + ], + ), + ["A1", "A2", "A3", "A4", "A5", "A7"], + None, + None, + ), + ( + "test11", + None, + ( + ["Animal", "Color"], + [ + ["Cat", "Blue"], + ["Dog", "Green"], + ["Mouse", "Yellow"], + ["Bird", "Purple"], + ["Cow", "Red"], + ], + ), + None, + None, + None, + ), + ], +) +def test_parametrization( + csv_file, id_col, result, ids, expect_exception, expect_message +): # pylint: disable=too-many-arguments + """ + Test the parametrization method + """ + data_file = join(dirname(__file__), "assets", f"{csv_file}.csv") + if expect_exception: + with pytest.raises(expect_exception) as raised_exception: + add_parametrization(data_file=data_file, id_col=id_col, data_casts={"Number": int}) + if expect_message is not None: + assert raised_exception.value.args[0] == expect_message + else: + parameterization = add_parametrization(data_file=data_file, id_col=id_col, data_casts={"Number": int}) + assert parameterization is not None + assert isinstance(parameterization, type(pytest.mark.parametrize)) + assert parameterization.mark.args == parameterization.args + assert parameterization.args == result + assert parameterization.kwargs.get("ids", None) == ids diff --git a/tests/test_read_csv.py b/tests/test_read_csv.py new file mode 100644 index 0000000..762bc6a --- /dev/null +++ b/tests/test_read_csv.py @@ -0,0 +1,48 @@ +""" +Test the reading of the CSV +""" +from os.path import dirname, join + +import pytest + +from _ptcsvp.parametrize import read_csv +from pytest_csv_params.dialect import CsvParamsDefaultDialect +from pytest_csv_params.exception import CsvParamsDataFileInvalid, CsvParamsDataFileNotFound + + +@pytest.mark.parametrize( + ["csv_file", "base_dir", "expect_lines", "expect_exception", "expect_message"], + [ + ("test1.csv", join(dirname(__file__), "assets"), 6, None, None), + (join(dirname(__file__), "assets", "test1.csv"), None, 6, None, None), + ("test2.csv", join(dirname(__file__), "assets"), 6, None, None), + (join(dirname(__file__), "assets", "test2.csv"), None, 6, None, None), + ("test3.csv", join(dirname(__file__), "assets"), 6, None, None), + (join(dirname(__file__), "assets", "test3.csv"), None, 6, None, None), + ("test4.csv", join(dirname(__file__), "assets"), -1, CsvParamsDataFileInvalid, "Invalid data"), + (join(dirname(__file__), "assets", "test4.csv"), None, -1, CsvParamsDataFileInvalid, "Invalid data"), + ("test5.csv", join(dirname(__file__), "assets"), 6, None, None), + (join(dirname(__file__), "assets", "test5.csv"), None, 6, None, None), + ("test-does-not-exist.csv", join(dirname(__file__), "assets"), -1, CsvParamsDataFileNotFound, None), + (join(dirname(__file__), "assets", "does-not-exist.csv"), None, -1, CsvParamsDataFileNotFound, None), + ("test-does-not-exist.csv", join(dirname(__file__), "does-not-exist"), -1, CsvParamsDataFileNotFound, None), + (join(dirname(__file__), "does-not-exist", "does-not-exist.csv"), None, -1, CsvParamsDataFileNotFound, None), + ("test1.csv", join(dirname(__file__), "does-not-exist"), -1, CsvParamsDataFileNotFound, None), + (join(dirname(__file__), "does-not-exist", "test1.csv"), None, -1, CsvParamsDataFileNotFound, None), + (None, join(dirname(__file__), "assets"), -1, CsvParamsDataFileInvalid, None), + (None, None, -1, CsvParamsDataFileInvalid, None), + ], +) +def test_csv_reader(csv_file, base_dir, expect_lines, expect_exception, expect_message): + """ + Test behaviour of the CSV loading + """ + + if expect_exception is not None: + with pytest.raises(expect_exception) as raised_exception: + read_csv(base_dir, csv_file, CsvParamsDefaultDialect) + if expect_message is not None: + assert raised_exception.value.args[0] == expect_message + else: + csv_lines = read_csv(base_dir, csv_file, CsvParamsDefaultDialect) + assert len(csv_lines) == expect_lines diff --git a/tests/test_version_check.py b/tests/test_version_check.py new file mode 100644 index 0000000..e639c00 --- /dev/null +++ b/tests/test_version_check.py @@ -0,0 +1,118 @@ +""" +We are checking the python version for the plugin. +""" +import sys + +import pytest +from attr.exceptions import PythonTooOldError + +from _ptcsvp.version import check_pytest_version, check_python_version + + +def build_version(p_version): + """ + Build a Version + """ + elements = [] + for v_part in p_version.split("."): + try: + elements.append(int(v_part)) + except ValueError: + elements.append(v_part) + return tuple(elements) + + +@pytest.mark.parametrize( + ["p_version", "expect_error"], + [ + ("3.8", None), + ("3.8.1", None), + ("3.8.10", None), + ("3.8.10.ray.x", None), + ("3.9", None), + ("3.9.2", None), + ("3.9.21", None), + ("3.9.21.pre.7", None), + ("3.10", None), + ("3.10.31", None), + ("3.10.31.beta.7", None), + ("3.11", None), + ("3.11.12", None), + ("3.11.12.alpha.4", None), + ("3.12", None), + ("3.12.22", None), + ("3.12.22.final", None), + ("3.12.22.final.0", None), + ("2.7", (PythonTooOldError, "At least Python 3.8 required")), + ("2.7.1", (PythonTooOldError, "At least Python 3.8 required")), + ("2.7.9", (PythonTooOldError, "At least Python 3.8 required")), + ("2.7.9.final", (PythonTooOldError, "At least Python 3.8 required")), + ("2.7.9.final.6", (PythonTooOldError, "At least Python 3.8 required")), + ("3.7", (PythonTooOldError, "At least Python 3.8 required")), + ("3.7.1", (PythonTooOldError, "At least Python 3.8 required")), + ("3.7.final", (PythonTooOldError, "At least Python 3.8 required")), + ("3.7.final.6", (PythonTooOldError, "At least Python 3.8 required")), + ("3.7.final.6.777", (PythonTooOldError, "At least Python 3.8 required")), + ("3.7.3.final.6.777", (PythonTooOldError, "At least Python 3.8 required")), + ("3.7.3.7.final.6.777", (PythonTooOldError, "At least Python 3.8 required")), + ("3", (PythonTooOldError, "At least Python 3.8 required")), + ], +) +def test_python_version(mocker, p_version, expect_error): + """ + Test python versions + """ + mocker.patch.object(sys, "version_info", build_version(p_version)) + if expect_error is not None: + with pytest.raises(expect_error[0]) as raised_exc: + check_python_version() + assert raised_exc.value.args[0] == expect_error[1] + else: + check_python_version() + + +@pytest.mark.parametrize( + ["p_version", "expect_error"], + [ + ("7.1", None), + ("7.1.1", None), + ("7.1.1.2", None), + ("7.1.1.2.3", None), + ("7.1.1.455555", None), + ("7.1.1.2-145", None), + ("7.1.1.090909090990", None), + ("7.2", None), + ("7.2.5", None), + ("7.2.5.6", None), + ("7.2.5.6.3333333", None), + ("7.2.5.6.333333333.4", None), + ("8.0", None), + ("8.0.1", None), + ("8.0.beta", None), + ("8.0.beta.3", None), + ("8.0.2.3", None), + ("7.0", (RuntimeError, "At least Pytest 7.1 required")), + ("7.0.1", (RuntimeError, "At least Pytest 7.1 required")), + ("7.0.111111", (RuntimeError, "At least Pytest 7.1 required")), + ("7.0.111111.extra", (RuntimeError, "At least Pytest 7.1 required")), + ("7.0.111111.extra.git-22", (RuntimeError, "At least Pytest 7.1 required")), + ("7", (RuntimeError, "At least Pytest 7.1 required")), + ("6", (RuntimeError, "At least Pytest 7.1 required")), + ("6.1", (RuntimeError, "At least Pytest 7.1 required")), + ("6.1.2", (RuntimeError, "At least Pytest 7.1 required")), + ("6.1.2.final", (RuntimeError, "At least Pytest 7.1 required")), + ("6.1.2.final.3", (RuntimeError, "At least Pytest 7.1 required")), + ], +) +def test_pytest_version(mocker, p_version, expect_error): + """ + Test pytest versions + """ + mocker.patch.object(pytest, "__version__", p_version) + assert pytest.__version__ == p_version + if expect_error is not None: + with pytest.raises(expect_error[0]) as raised_exc: + check_pytest_version() + assert raised_exc.value.args[0] == expect_error[1] + else: + check_pytest_version()