From c704e116cc921f9a91b85e897417d573d5f01eb7 Mon Sep 17 00:00:00 2001 From: Tom Oram Date: Wed, 23 Jan 2019 15:09:23 +0000 Subject: [PATCH] Starting point --- .gitignore | 117 ++++++ .pylintrc | 564 +++++++++++++++++++++++++++ Pipfile | 15 + Pipfile.lock | 185 +++++++++ README.md | 66 ++++ features/environment.py | 3 + features/message_feed.feature | 54 +++ features/steps/message_feed_steps.py | 31 ++ flitter/__init__.py | 0 flitter/flitter.py | 36 ++ pycharm-test-config.png | Bin 0 -> 27499 bytes tests/test_example.py | 2 + 12 files changed, 1073 insertions(+) create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md create mode 100644 features/environment.py create mode 100644 features/message_feed.feature create mode 100644 features/steps/message_feed_steps.py create mode 100644 flitter/__init__.py create mode 100644 flitter/flitter.py create mode 100644 pycharm-test-config.png create mode 100644 tests/test_example.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e5c966 --- /dev/null +++ b/.gitignore @@ -0,0 +1,117 @@ +# 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 +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# 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/ + +.idea/ \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..cfb3b1f --- /dev/null +++ b/.pylintrc @@ -0,0 +1,564 @@ +[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 blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. 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. Specifying 0 will auto-detect the +# number of processors available to use. +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 modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# 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 + + +[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=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + 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, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + missing-docstring, + bare-except, + invalid-name + +# 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 + + +[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, e.g. +# 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 + +# 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 + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# 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 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 + + +[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 + +# 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 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= + +# 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 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 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=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 style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# 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. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-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. +#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. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# 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. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-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. +#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 + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[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=cls + + +[DESIGN] + +# 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. +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=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception". +overgeneral-exceptions=Exception diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..ae31b42 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +behave = "*" +pylint = "*" +pytest = "*" +flake8 = "*" + +[packages] + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..fc02b66 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,185 @@ +{ + "_meta": { + "hash": { + "sha256": "cf45f87a01d4c6e579529c1543a7dd9171ed0650b466ac3e91b9f5382fd79fe0" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "atomicwrites": { + "hashes": [ + "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", + "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" + ], + "version": "==1.2.1" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, + "behave": { + "hashes": [ + "sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", + "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c" + ], + "index": "pypi", + "version": "==1.2.6" + }, + "flake8": { + "hashes": [ + "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", + "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" + ], + "index": "pypi", + "version": "==3.6.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", + "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", + "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9" + ], + "version": "==5.0.0" + }, + "parse": { + "hashes": [ + "sha256:870dd675c1ee8951db3e29b81ebe44fd131e3eb8c03a79483a58ea574f3145c2" + ], + "version": "==1.11.1" + }, + "parse-type": { + "hashes": [ + "sha256:6e906a66f340252e4c324914a60d417d33a4bea01292ea9bbf68b4fc123be8c9", + "sha256:f596bdc75d3dd93036fbfe3d04127da9f6df0c26c36e01e76da85adef4336b3c" + ], + "version": "==0.4.2" + }, + "pluggy": { + "hashes": [ + "sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", + "sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a" + ], + "version": "==0.8.1" + }, + "py": { + "hashes": [ + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + ], + "version": "==1.7.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", + "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" + ], + "version": "==2.4.0" + }, + "pyflakes": { + "hashes": [ + "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", + "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" + ], + "version": "==2.0.0" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "pytest": { + "hashes": [ + "sha256:41568ea7ecb4a68d7f63837cf65b92ce8d0105e43196ff2b26622995bb3dc4b2", + "sha256:c3c573a29d7c9547fb90217ece8a8843aa0c1328a797e200290dc3d0b4b823be" + ], + "index": "pypi", + "version": "==4.1.1" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..d725140 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# BDD Lab (Social Network) + +This is the source material for the BDD lab. + +The goal of this lab is to show how to use human readable specifications to +drive the development of a system - by using [behave](https://github.com/behave/behave), the cucumber-clone BDD framework for Python, +to make the specifications into automated tests. + +Instructions on how to do this lab can be found in the +[exercises repository](https://github.com/ONSdigital/onse-training-exercises/blob/master/onse-lab-bdd.md). + +### Development Setup + +Make sure you have the latest version of Python installed (currently 3.7.1) from +the [Python website](https://www.python.org/downloads/release/python-371/). + +From within the repository folder, please install the following in order to run +the project: + +```bash +# install the dependencies from Pipfile: +pipenv install --dev + +# activate this project's virtualenv: +pipenv shell +``` + +Pipenv "automatically creates and manages a virtualenv for your projects, as +well as adds/removes packages from your Pipfile as you install/uninstall +packages. It also generates the ever-important Pipfile.lock, which is used to +produce deterministic builds." [1](https://pipenv.readthedocs.io/en/latest/) + + +## Running tests + +The following commands can be executed inside your `pipenv shell`. + +### Style & Linting + +```bash +flake8 +``` + +### Unit Tests + +```bash +python -m pytest +``` + +### Acceptance Tests (features) + +```bash +behave +``` + +## PyCharm Run Configuration + +Add a run configuration of type `Python tests -> pytest` with the following +settings: + +![PyCharm Test Config](pycharm-test-config.png) + +**REMARK**: + +The BDD support is available only in the PyCharm Professional Edition, not in +the Community Edition. diff --git a/features/environment.py b/features/environment.py new file mode 100644 index 0000000..3a0da92 --- /dev/null +++ b/features/environment.py @@ -0,0 +1,3 @@ +def before_scenario(context, step): + context.errorMessage = 'Unimplemented step. ' + \ + 'Please provide the implementation.' diff --git a/features/message_feed.feature b/features/message_feed.feature new file mode 100644 index 0000000..68c39d8 --- /dev/null +++ b/features/message_feed.feature @@ -0,0 +1,54 @@ +Feature: Posting messages + As a social person + I want to see a list of message from all the people I'm interested in + So that I'm kept up to date with all the exciting things that they are doing + + # Rule: Users see messages they write themselves + + Scenario: The user sees their own messages + Given Alice has posted a message "Hello fans" + And Alice has posted a message "It's a lovely day today" + When Alice views their feed + Then they can see the message "Hello fans" by Alice + And they can see the message "It's a lovely day today" by Alice + + # Rule: Users see messages written by people they follow + +# Scenario: The user doesn't see message from people they're not following +# Given Bob has posted a message "Bob here" +# And Alice is not following Bob +# When Alice views their feed +# Then they cannot see the message "Bob here" by Bob +# +# Scenario: The user sees a message from someone they follow +# Given Bob has posted a message "Bob here again" +# And Alice follows Bob +# When Alice views their feed +# Then they can see the message "Bob here again" by Bob + + # Rule: Users see messages which mention them + +# Scenario: The user has been mentioned in a message +# Given Alice has posted a message "I like @Bob" +# When Bob views their feed +# Then they can see the message "I like @Bob" by Alice +# +# Scenario: The user with a similar name has been mentioned +# Given Alice has posted a message "I like @BobHope" +# When Bob views their feed +# Then they cannot see the message "I like @BobHope" by Alice +# +# Scenario: The user is mentioned in the middle of a message +# Given Alice has posted a message "I like @Bob a lot" +# When Bob views their feed +# Then they can see the message "I like @Bob a lot" by Alice +# +# Scenario: The user mention is in a different case +# Given Alice has posted a message "I like @bOb" +# When Bob views their feed +# Then they can see the message "I like @bOb" by Alice +# +# Scenario: The user mention is not a separated word +# Given Alice has posted a message "Ilike@Bob" +# When Bob views their feed +# Then they cannot see the message "Ilike@Bob" by Alice diff --git a/features/steps/message_feed_steps.py b/features/steps/message_feed_steps.py new file mode 100644 index 0000000..f404672 --- /dev/null +++ b/features/steps/message_feed_steps.py @@ -0,0 +1,31 @@ +from behave import given, when, then + + +@given('{author} has posted a message "{message}"') +def post_message(context, author, message): + raise NotImplementedError(context.errorMessage) + + +@given('{follower} follows {followee}') +def follow(context, follower, followee): + raise NotImplementedError(context.errorMessage) + + +@given('{follower} is not following {followee}') +def do_not_follow(context, follower, followee): + raise NotImplementedError(context.errorMessage) + + +@when('{user} views their feed') +def view_feed(context, user): + raise NotImplementedError(context.errorMessage) + + +@then('they can see the message "{message}" by {author}') +def assert_can_see_message(context, message, author): + raise NotImplementedError(context.errorMessage) + + +@then('they cannot see the message "{message}" by {author}') +def assert_cannot_see_message(context, message, author): + raise NotImplementedError(context.errorMessage) diff --git a/flitter/__init__.py b/flitter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flitter/flitter.py b/flitter/flitter.py new file mode 100644 index 0000000..74b2a9b --- /dev/null +++ b/flitter/flitter.py @@ -0,0 +1,36 @@ +class Flitter: + def post(self, author, message): + """ + Post a message + + :param author: The user name of the author + :type author: string + :param message: The message to be posted + :type message: string + :return: nothing + :rtype: void + """ + pass + + def get_feed_for(self, user): + """ + Get messages in a users feed. + + :param user: The user to get messages for + :type user: string + :return: All the message as a list of dicts containing author and message + :rtype: list(dict(author=string, message=string)) + """ + pass + + def follow(self, follower, followee): + """ + Make one user follow another + :param follower: The user who is following + :type follower: string + :param followee: The user being followed + :type followee: string + :return: nothing + :rtype: void + """ + pass diff --git a/pycharm-test-config.png b/pycharm-test-config.png new file mode 100644 index 0000000000000000000000000000000000000000..f49181270d244840f5ed7122519099134445e5c0 GIT binary patch literal 27499 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 literal 0 HcmV?d00001 diff --git a/tests/test_example.py b/tests/test_example.py new file mode 100644 index 0000000..3f4aafd --- /dev/null +++ b/tests/test_example.py @@ -0,0 +1,2 @@ +def test_2_plus_2_is_4(): + assert 2 + 2 == 4