diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..c286601
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,74 @@
+version: 2
+
+jobs:
+ "python-2.7": &test-template
+ docker:
+ - image: circleci/python:2.7-stretch-browsers
+ environment:
+ REQUIREMENTS_FILE: dev-requirements.txt
+ PYLINTRC: .pylintrc
+ TOX: py27,py27dj{109,110,111,200,201}
+ TOX_PYTHON_27: python
+
+ steps:
+ - checkout
+
+ - run:
+ name: Write deps cache key
+ command: cat "$REQUIREMENTS_FILE" > reqs.txt
+
+ - run:
+ name: Write job name
+ command: echo $CIRCLE_JOB > circlejob.txt
+
+ - restore_cache:
+ key: deps1-{{ .Branch }}-{{ checksum "reqs.txt" }}-{{ checksum ".circleci/config.yml" }}-{{ checksum "circlejob.txt" }}
+
+ - run:
+ name: Install dependencies
+ command: |
+ sudo pip install virtualenv
+ virtualenv venv
+ . venv/bin/activate
+ pip install tox
+
+ - save_cache:
+ key: deps1-{{ .Branch }}-{{ checksum "reqs.txt" }}-{{ checksum ".circleci/config.yml" }}-{{ checksum "circlejob.txt" }}
+ paths:
+ - "venv"
+ - ".tox"
+
+ - run:
+ name: Run tox
+ command: |
+ . venv/bin/activate
+ tox tox.ini -e $TOX
+
+ "python-3.6":
+ <<: *test-template
+ docker:
+ - image: circleci/python:3.6-stretch-browsers
+ environment:
+ REQUIREMENTS_FILE: dev-requirements.txt
+ PYLINTRC: .pylintrc
+ TOX: py36,py36dj{109,110,111,200,201}
+ TOX_PYTHON_36: python
+
+ "python-3.7":
+ <<: *test-template
+ docker:
+ - image: circleci/python:3.7-stretch-browsers
+ environment:
+ REQUIREMENTS_FILE: dev-requirements.txt
+ PYLINTRC37: .pylintrc37
+ TOX: py37,py37dj{109,110,111,200,201}
+ TOX_PYTHON_37: python
+
+
+workflows:
+ version: 2
+ build:
+ jobs:
+ - "python-2.7"
+ - "python-3.6"
+ - "python-3.7"
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..6deafc2
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 120
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..dd94b77
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,7 @@
+Thanks so much for your interest in Dash!
+Before you post an issue here, could you quickly search for your
+issue in the Dash community forum? https://community.plot.ly/c/dash
+GitHub issues are great for bug reports, the community forum is great for
+implementation questions. When in doubt, creating feel free to just post the
+issue here :)
+Thanks, and welcome to Dash!
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5230a5f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+*.pyc
+*.ipynb_notebooks
+*.ipynb
+ignore
+*~
+venv
+build/
+dist/
+lib/
+node_modules/
+.npm
+vv/
+venv/
+*.pyc
+*.egg-info
+*.log
+.DS_Store
+dist
+*egg-info*
+npm-debug*
+/.tox
+.idea
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..eab8cff
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,467 @@
+[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.
+jobs=1
+
+# 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=no
+
+# 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=fixme,
+ missing-docstring,
+ invalid-name,
+ useless-object-inheritance,
+ possibly-unused-variable
+# 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, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+
+[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.
+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=
+
+
+[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
+
+# 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
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+
+[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
+
+
+[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
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins
+
+
+[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 a if statement
+max-bool-expr=5
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[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=regsub,
+ TERMIOS,
+ Bastion,
+ rexec
+
+# 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
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/.pylintrc37 b/.pylintrc37
new file mode 100644
index 0000000..06e164b
--- /dev/null
+++ b/.pylintrc37
@@ -0,0 +1,568 @@
+[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=no
+
+# 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=invalid-name,
+ missing-docstring,
+ 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,
+ locally-enabled,
+ file-ignored,
+ suppressed-message,
+ useless-suppression,
+ deprecated-pragma,
+ use-symbolic-message-instead,
+ fixme,
+ 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,
+ no-else-return,
+ useless-object-inheritance,
+ possibly-unused-variable,
+ too-many-lines
+
+# 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
+
+
+[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=
+
+
+[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
+
+# 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
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+
+[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
+
+
+[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
+
+
+[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 to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. 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
+
+
+[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
+
+
+[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=regsub,
+ TERMIOS,
+ Bastion,
+ rexec
+
+# 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
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception".
+overgeneral-exceptions=Exception
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0c1b83a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Sergei Pikhovkin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..04f196a
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include README.md
+include LICENSE
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8431e6e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,46 @@
+# dj-plotly-dash
+
+[](https://circleci.com/gh/pikhovkin/dj-plotly-dash)
+[](https://pypi.org/project/dj-plotly-dash/)
+
+[](https://www.djangoproject.com/)
+[](./LICENSE)
+
+
+#### Dash is a Python framework for building analytical web applications. No JavaScript required.
+
+It's fork of Plotly [Dash](https://github.com/plotly/dash).
+
+Here’s [a example of view of Django Dash App](https://gist.github.com/pikhovkin/6ec23d425b12b720651942fd6a5cdf13) ([original example with Flask](https://gist.github.com/chriddyp/3d2454905d8f01886d651f207e2419f0)) that ties a Dropdown to a D3.js Plotly Graph.
+As the user selects a value in the Dropdown, the application code dynamically
+exports data from Google Finance into a Pandas DataFrame.
+
+
+
+Dash app code is declarative and reactive, which makes it easy to build complex apps that contain many interactive elements. Here’s an example ([original example with Flask](https://gist.github.com/chriddyp/9b2b3e8a6c67697279d3724dce5dab3c)) with 5 inputs, 3 outputs, and cross filtering. This app was composed in just 160 lines of code, all of which were Python.
+
+
+
+Dash uses [Plotly.js](https://github.com/plotly/plotly.js) for charting. Over 35 chart types are supported, including maps.
+
+Dash isn't just for dashboards. You have full control over the look and feel of your applications. Here's a Dash app that's styled to look like a PDF report.
+
+To learn more about Dash, read the [extensive announcement letter](https://medium.com/@plotlygraphs/introducing-dash-5ecf7191b503) or [jump in with the user guide](https://plot.ly/dash).
+
+### Usage
+
+See examples of usage in `tests/django_project`
+
+### Installation
+
+ pip install dj-plotly-dash
+ pip install dash_core_components>=0.30.2 --no-deps
+ pip install dash_html_components>=0.13.2 --no-deps
+
+### Documentation
+
+View the [Dash User Guide](https://plot.ly/dash). It's chock-full of examples, pro tips, and guiding principles.
+
+### Licensing
+
+MIT
diff --git a/dash/__init__.py b/dash/__init__.py
new file mode 100644
index 0000000..027dbb8
--- /dev/null
+++ b/dash/__init__.py
@@ -0,0 +1,6 @@
+from .dash import Dash, BaseDashView # noqa: F401
+from . import dependencies # noqa: F401
+from . import development # noqa: F401
+from . import exceptions # noqa: F401
+from . import resources # noqa: F401
+from .version import __version__ # noqa: F401
diff --git a/dash/_utils.py b/dash/_utils.py
new file mode 100644
index 0000000..17dc247
--- /dev/null
+++ b/dash/_utils.py
@@ -0,0 +1,74 @@
+def interpolate_str(template, **data):
+ s = template
+ for k, v in data.items():
+ key = '{%' + k + '%}'
+ s = s.replace(key, v)
+ return s
+
+
+def format_tag(tag_name, attributes, inner='', closed=False, opened=False):
+ tag = '<{tag} {attributes}'
+ if closed:
+ tag += '/>'
+ elif opened:
+ tag += '>'
+ else:
+ tag += '>' + inner + '{tag}>'
+ return tag.format(
+ tag=tag_name,
+ attributes=' '.join([
+ '{}="{}"'.format(k, v) for k, v in attributes.items()]))
+
+
+def get_asset_path(
+ requests_pathname,
+ routes_pathname,
+ asset_path,
+ asset_url_path):
+
+ i = requests_pathname.rfind(routes_pathname)
+ req = requests_pathname[:i]
+
+ return '/'.join([
+ # Only take the first part of the pathname
+ req,
+ asset_url_path,
+ asset_path
+ ])
+
+
+class AttributeDict(dict):
+ """
+ Dictionary subclass enabling attribute lookup/assignment of keys/values.
+
+ For example::
+ >>> m = AttributeDict({'foo': 'bar'})
+ >>> m.foo
+ 'bar'
+ >>> m.foo = 'not bar'
+ >>> m['foo']
+ 'not bar'
+ ``AttributeDict`` objects also provide ``.first()`` which acts like
+ ``.get()`` but accepts multiple keys as arguments, and returns the value of
+ the first hit, e.g.::
+ >>> m = AttributeDict({'foo': 'bar', 'biz': 'baz'})
+ >>> m.first('wrong', 'incorrect', 'foo', 'biz')
+ 'bar'
+ """
+
+ def __setattr__(self, key, value):
+ self[key] = value
+
+ def __getattr__(self, key):
+ try:
+ return self[key]
+ except KeyError:
+ # to conform with __getattr__ spec
+ raise AttributeError(key)
+
+ # pylint: disable=inconsistent-return-statements
+ def first(self, *names):
+ for name in names:
+ value = self.get(name)
+ if value:
+ return value
diff --git a/dash/authentication.py b/dash/authentication.py
new file mode 100644
index 0000000..e69de29
diff --git a/dash/dash.py b/dash/dash.py
new file mode 100644
index 0000000..0e0a1f0
--- /dev/null
+++ b/dash/dash.py
@@ -0,0 +1,944 @@
+from __future__ import print_function
+
+import os
+import sys
+import collections
+import importlib
+import json
+import pkgutil
+from functools import wraps
+import re
+
+import plotly
+import dash_renderer
+import six
+
+from django.contrib.staticfiles.utils import get_files
+from django.contrib.staticfiles.storage import staticfiles_storage
+from django.http import HttpResponse, JsonResponse as BaseJsonResponse
+from django.views.decorators.csrf import csrf_exempt
+from django.views.generic import View
+
+from .dependencies import Event, Input, Output, State
+from .resources import Scripts, Css
+from .development.base_component import Component
+from . import exceptions
+from ._utils import AttributeDict as _AttributeDict
+from ._utils import interpolate_str as _interpolate
+from ._utils import format_tag as _format_tag
+
+
+__all__ = (
+ 'JsonResponse',
+ 'MetaDashView',
+ 'Dash',
+ 'BaseDashView'
+)
+
+
+class JsonResponse(BaseJsonResponse):
+ def __init__(self, data, encoder=plotly.utils.PlotlyJSONEncoder, safe=False,
+ json_dumps_params=None, **kwargs):
+ super(JsonResponse, self).__init__(data, encoder=encoder, safe=safe,
+ json_dumps_params=json_dumps_params, **kwargs)
+
+
+class MetaDashView(type):
+ def __new__(cls, name, bases, attrs):
+ new_cls = super(MetaDashView, cls).__new__(cls, name, bases, attrs)
+
+ if new_cls.__dict__.get('dash_name', ''):
+ new_cls._dashes[new_cls.__dict__['dash_name']] = new_cls # pylint: disable=protected-access
+ dash_prefix = getattr(new_cls, 'dash_prefix', '').strip()
+ if dash_prefix:
+ # pylint: disable=protected-access
+ new_cls._dashes[dash_prefix + new_cls.__dict__['dash_name']] = new_cls
+
+ return new_cls
+
+
+_default_index = '''
+
+
+
+ {%metas%}
+ {%title%}
+ {%favicon%}
+ {%css%}
+
+
+ {%app_entry%}
+
+ {%config%}
+ {%scripts%}
+
+
+
+'''
+
+_app_entry = '''
+
+'''
+
+_re_index_entry = re.compile(r'{%app_entry%}')
+_re_index_config = re.compile(r'{%config%}')
+_re_index_scripts = re.compile(r'{%scripts%}')
+
+_re_index_entry_id = re.compile(r'id="react-entry-point"')
+_re_index_config_id = re.compile(r'id="_dash-config"')
+_re_index_scripts_id = re.compile(r'src=".*dash[-_]renderer.*"')
+
+
+# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-locals
+class Dash(object):
+ # pylint: disable=unused-argument
+ def __init__(self, url_base_pathname='/',
+ meta_tags=None,
+ index_string=_default_index,
+ external_scripts=None,
+ external_stylesheets=None,
+ assets_folder=None,
+ assets_ignore=None,
+ components_cache_max_age=None,
+ **kwargs):
+ self._assets_folder = assets_folder
+
+ self.url_base_pathname = url_base_pathname
+ self.config = _AttributeDict({
+ 'suppress_callback_exceptions': False,
+ 'routes_pathname_prefix': url_base_pathname,
+ 'requests_pathname_prefix': url_base_pathname,
+ 'assets_external_path': None,
+ 'components_cache_max_age': components_cache_max_age or 2678400
+ })
+
+ # list of dependencies
+ self.callback_map = {}
+
+ self._index_string = ''
+ self.index_string = index_string
+ self._meta_tags = meta_tags or []
+ self._favicon = None
+
+ # static files from the packages
+ self.css = Css()
+ self.scripts = Scripts()
+
+ self._external_scripts = external_scripts or []
+ self._external_stylesheets = external_stylesheets or []
+
+ self.assets_ignore = assets_ignore
+
+ self.registered_paths = {}
+
+ self._layout = None
+ self._cached_layout = None
+ self.routes = []
+
+ self._dev_tools = _AttributeDict({
+ 'serve_dev_bundles': False
+ })
+
+ @property
+ def index_string(self):
+ return self._index_string
+
+ @index_string.setter
+ def index_string(self, value):
+ checks = (
+ (_re_index_entry.search(value), 'app_entry'),
+ (_re_index_config.search(value), 'config',),
+ (_re_index_scripts.search(value), 'scripts'),
+ )
+ missing = [missing for check, missing in checks if not check]
+ if missing:
+ raise Exception(
+ 'Did you forget to include {} in your index string ?'.format(
+ ', '.join('{%' + x + '%}' for x in missing)
+ )
+ )
+ self._index_string = value
+
+ @property
+ def layout(self):
+ return self._layout
+
+ def _layout_value(self):
+ if isinstance(self._layout, collections.Callable):
+ self._cached_layout = self._layout()
+ else:
+ self._cached_layout = self._layout
+ return self._cached_layout
+
+ @layout.setter
+ def layout(self, value):
+ if (not isinstance(value, Component) and
+ not isinstance(value, collections.Callable)):
+ raise Exception(
+ ''
+ 'Layout must be a dash component '
+ 'or a function that returns '
+ 'a dash component.')
+
+ self._layout = value
+
+ self._validate_layout()
+
+ layout_value = self._layout_value()
+ # pylint: disable=protected-access
+ self.css._update_layout(layout_value)
+ self.scripts._update_layout(layout_value)
+
+ def _config(self):
+ return {
+ 'url_base_pathname': self.url_base_pathname,
+ 'requests_pathname_prefix': self.config.requests_pathname_prefix
+ }
+
+ def _collect_and_register_resources(self, resources):
+ # now needs the app context.
+ # template in the necessary component suite JS bundles
+ # add the version number of the package as a query parameter
+ # for cache busting
+ def _relative_url_path(relative_package_path='', namespace=''):
+
+ # track the registered packages
+ if namespace in self.registered_paths:
+ self.registered_paths[namespace].append(relative_package_path)
+ else:
+ self.registered_paths[namespace] = [relative_package_path]
+
+ module_path = os.path.join(
+ os.path.dirname(sys.modules[namespace].__file__),
+ relative_package_path)
+
+ modified = int(os.stat(module_path).st_mtime)
+
+ return '{}_dash-component-suites/{}/{}?v={}&m={}'.format(
+ self.config['requests_pathname_prefix'],
+ namespace,
+ relative_package_path,
+ importlib.import_module(namespace).__version__,
+ modified
+ )
+
+ srcs = []
+ for resource in resources:
+ if 'relative_package_path' in resource:
+ if isinstance(resource['relative_package_path'], str):
+ srcs.append(_relative_url_path(**resource))
+ else:
+ for rel_path in resource['relative_package_path']:
+ srcs.append(_relative_url_path(
+ relative_package_path=rel_path,
+ namespace=resource['namespace']
+ ))
+ elif 'external_url' in resource:
+ if isinstance(resource['external_url'], str):
+ srcs.append(resource['external_url'])
+ else:
+ for url in resource['external_url']:
+ srcs.append(url)
+ elif 'absolute_path' in resource:
+ raise Exception(
+ 'Serving files from absolute_path isn\'t supported yet'
+ )
+ elif 'asset_path' in resource:
+ static_url = resource['asset_path']
+ # Add a bust query param
+ static_url += '?m={}'.format(resource['ts'])
+ srcs.append(static_url)
+ return srcs
+
+ def _generate_css_dist_html(self):
+ links = self._external_stylesheets + \
+ self._collect_and_register_resources(self.css.get_all_css())
+
+ return '\n'.join([
+ _format_tag('link', link, opened=True)
+ if isinstance(link, dict)
+ else ' '.format(link)
+ for link in links
+ ])
+
+ def _generate_scripts_html(self):
+ # Dash renderer has dependencies like React which need to be rendered
+ # before every other script. However, the dash renderer bundle
+ # itself needs to be rendered after all of the component's
+ # scripts have rendered.
+ # The rest of the scripts can just be loaded after React but before
+ # dash renderer.
+ # pylint: disable=protected-access
+ srcs = self._collect_and_register_resources(
+ self.scripts._resources._filter_resources(
+ dash_renderer._js_dist_dependencies,
+ dev_bundles=self._dev_tools.serve_dev_bundles
+ )) + self._external_scripts + self._collect_and_register_resources(
+ self.scripts.get_all_scripts(
+ dev_bundles=self._dev_tools.serve_dev_bundles) +
+ self.scripts._resources._filter_resources(
+ dash_renderer._js_dist,
+ dev_bundles=self._dev_tools.serve_dev_bundles
+ ))
+
+ return '\n'.join([
+ _format_tag('script', src)
+ if isinstance(src, dict)
+ else ''.format(src)
+ for src in srcs
+ ])
+
+ def _generate_config_html(self, **kwargs):
+ config = self._config()
+ config.update(kwargs)
+ return (
+ ''
+ ).format(json.dumps(config, cls=plotly.utils.PlotlyJSONEncoder))
+
+ def _generate_meta_html(self):
+ has_ie_compat = any(
+ x.get('http-equiv', '') == 'X-UA-Compatible'
+ for x in self._meta_tags)
+ has_charset = any('charset' in x for x in self._meta_tags)
+
+ tags = []
+ if not has_ie_compat:
+ tags.append(' ')
+ if not has_charset:
+ tags.append(' ')
+
+ tags = tags + [
+ _format_tag('meta', x, opened=True) for x in self._meta_tags
+ ]
+
+ return '\n '.join(tags)
+
+ def index(self, *args, **kwargs): # pylint: disable=unused-argument
+ if self._assets_folder:
+ self._walk_assets_directory()
+
+ scripts = self._generate_scripts_html()
+ css = self._generate_css_dist_html()
+ config = self._generate_config_html()
+ metas = self._generate_meta_html()
+ title = getattr(self, 'title', 'Dash')
+ if self._favicon:
+ favicon = ' '.format(self._favicon)
+ else:
+ favicon = ''
+
+ index = self.interpolate_index(
+ metas=metas, title=title, css=css, config=config,
+ scripts=scripts, app_entry=_app_entry, favicon=favicon)
+
+ checks = (
+ (_re_index_entry_id.search(index), '#react-entry-point'),
+ (_re_index_config_id.search(index), '#_dash-configs'),
+ (_re_index_scripts_id.search(index), 'dash-renderer'),
+ )
+ missing = [missing for check, missing in checks if not check]
+
+ if missing:
+ plural = 's' if len(missing) > 1 else ''
+ raise Exception(
+ 'Missing element{pl} {ids} in index.'.format(
+ ids=', '.join(missing),
+ pl=plural
+ )
+ )
+
+ return index
+
+ def interpolate_index(self,
+ metas='', title='', css='', config='',
+ scripts='', app_entry='', favicon=''):
+ """
+ Called to create the initial HTML string that is loaded on page.
+ Override this method to provide you own custom HTML.
+
+ :Example:
+
+ class MyDash(dash.Dash):
+ def interpolate_index(self, **kwargs):
+ return '''
+
+
+
+ My App
+
+
+
+ {app_entry}
+ {config}
+ {scripts}
+
+
+
+ '''.format(
+ app_entry=kwargs.get('app_entry'),
+ config=kwargs.get('config'),
+ scripts=kwargs.get('scripts'))
+
+ :param metas: Collected & formatted meta tags.
+ :param title: The title of the app.
+ :param css: Collected & formatted css dependencies as tags.
+ :param config: Configs needed by dash-renderer.
+ :param scripts: Collected & formatted scripts tags.
+ :param app_entry: Where the app will render.
+ :param favicon: A favicon tag if found in assets folder.
+ :return: The interpolated HTML string for the index.
+ """
+ return _interpolate(self.index_string,
+ metas=metas,
+ title=title,
+ css=css,
+ config=config,
+ scripts=scripts,
+ favicon=favicon,
+ app_entry=app_entry)
+
+ def dependencies(self, *args, **kwargs): # pylint: disable=unused-argument
+ return [
+ {
+ 'output': {
+ 'id': k.split('.')[0],
+ 'property': k.split('.')[1]
+ },
+ 'inputs': v['inputs'],
+ 'state': v['state'],
+ 'events': v['events']
+ } for k, v in list(self.callback_map.items())
+ ]
+
+ # pylint: disable=unused-argument, no-self-use
+ def react(self, *args, **kwargs):
+ raise exceptions.DashException(
+ 'Yo! `react` is no longer used. \n'
+ 'Use `callback` instead. `callback` has a new syntax too, '
+ 'so make sure to call `help(app.callback)` to learn more.')
+
+ # pylint: disable=unused-argument
+ def serve_component_suites(self, package_name, path_in_package_dist, *args, **kwargs):
+ """ Serve the JS bundles for each package
+ """
+ if package_name not in self.registered_paths:
+ raise exceptions.InvalidResourceError(
+ 'Error loading dependency.\n'
+ '"{}" is not a registered library.\n'
+ 'Registered libraries are: {}'
+ .format(package_name, list(self.registered_paths.keys())))
+
+ elif path_in_package_dist not in self.registered_paths[package_name]:
+ raise exceptions.InvalidResourceError(
+ '"{}" is registered but the path requested is not valid.\n'
+ 'The path requested: "{}"\n'
+ 'List of registered paths: {}'
+ .format(
+ package_name,
+ path_in_package_dist,
+ self.registered_paths
+ )
+ )
+
+ return pkgutil.get_data(package_name, path_in_package_dist)
+
+ def serve_routes(self, *args, **kwargs): # pylint: disable=unused-argument
+ return self.routes
+
+ def _validate_callback(self, output, inputs, state, events):
+ # pylint: disable=too-many-branches
+ layout = self._cached_layout or self._layout_value()
+
+ if (layout is None and
+ not self.config.first('suppress_callback_exceptions',
+ 'supress_callback_exceptions')):
+ # Without a layout, we can't do validation on the IDs and
+ # properties of the elements in the callback.
+ raise exceptions.LayoutIsNotDefined('''
+ Attempting to assign a callback to the application but
+ the `layout` property has not been assigned.
+ Assign the `layout` property before assigning callbacks.
+ Alternatively, suppress this warning by setting
+ `app.config['suppress_callback_exceptions']=True`
+ '''.replace(' ', ''))
+
+ for args, obj, name in [([output], Output, 'Output'),
+ (inputs, Input, 'Input'),
+ (state, State, 'State'),
+ (events, Event, 'Event')]:
+
+ if not isinstance(args, list):
+ raise exceptions.IncorrectTypeException(
+ 'The {} argument `{}` is '
+ 'not a list of `dash.dependencies.{}`s.'.format(
+ name.lower(), str(args), name
+ ))
+
+ for arg in args:
+ if not isinstance(arg, obj):
+ raise exceptions.IncorrectTypeException(
+ 'The {} argument `{}` is '
+ 'not of type `dash.{}`.'.format(
+ name.lower(), str(arg), name
+ ))
+
+ if (not self.config.first('suppress_callback_exceptions',
+ 'supress_callback_exceptions') and
+ arg.component_id not in layout and
+ arg.component_id != getattr(layout, 'id', None)):
+ raise exceptions.NonExistantIdException('''
+ Attempting to assign a callback to the
+ component with the id "{}" but no
+ components with id "{}" exist in the
+ app\'s layout.\n\n
+ Here is a list of IDs in layout:\n{}\n\n
+ If you are assigning callbacks to components
+ that are generated by other callbacks
+ (and therefore not in the initial layout), then
+ you can suppress this exception by setting
+ `app.config['suppress_callback_exceptions']=True`.
+ '''.format(
+ arg.component_id,
+ arg.component_id,
+ list(layout.keys()) + (
+ [] if not hasattr(layout, 'id') else
+ [layout.id]
+ )
+ ).replace(' ', ''))
+
+ if not self.config.first('suppress_callback_exceptions',
+ 'supress_callback_exceptions'):
+
+ if getattr(layout, 'id', None) == arg.component_id:
+ component = layout
+ else:
+ component = layout[arg.component_id]
+
+ if (hasattr(arg, 'component_property') and
+ arg.component_property not in
+ component.available_properties and not
+ any(arg.component_property.startswith(w) for w in
+ component.available_wildcard_properties)):
+ raise exceptions.NonExistantPropException('''
+ Attempting to assign a callback with
+ the property "{}" but the component
+ "{}" doesn't have "{}" as a property.\n
+ Here is a list of the available properties in "{}":
+ {}
+ '''.format(
+ arg.component_property,
+ arg.component_id,
+ arg.component_property,
+ arg.component_id,
+ component.available_properties).replace(
+ ' ', ''))
+
+ if (hasattr(arg, 'component_event') and
+ arg.component_event not in
+ component.available_events):
+ raise exceptions.NonExistantEventException('''
+ Attempting to assign a callback with
+ the event "{}" but the component
+ "{}" doesn't have "{}" as an event.\n
+ Here is a list of the available events in "{}":
+ {}
+ '''.format(
+ arg.component_event,
+ arg.component_id,
+ arg.component_event,
+ arg.component_id,
+ component.available_events).replace(' ', ''))
+
+ if state and not events and not inputs:
+ raise exceptions.MissingEventsException('''
+ This callback has {} `State` {}
+ but no `Input` elements or `Event` elements.\n
+ Without `Input` or `Event` elements, this callback
+ will never get called.\n
+ (Subscribing to input components will cause the
+ callback to be called whenver their values
+ change and subscribing to an event will cause the
+ callback to be called whenever the event is fired.)
+ '''.format(
+ len(state),
+ 'elements' if len(state) > 1 else 'element'
+ ).replace(' ', ''))
+
+ if '.' in output.component_id:
+ raise exceptions.IDsCantContainPeriods('''The Output element
+ `{}` contains a period in its ID.
+ Periods are not allowed in IDs right now.'''.format(
+ output.component_id
+ ))
+
+ callback_id = '{}.{}'.format(
+ output.component_id, output.component_property)
+ if callback_id in self.callback_map:
+ raise exceptions.CantHaveMultipleOutputs('''
+ You have already assigned a callback to the output
+ with ID "{}" and property "{}". An output can only have
+ a single callback function. Try combining your inputs and
+ callback functions together into one function.
+ '''.format(
+ output.component_id,
+ output.component_property).replace(' ', ''))
+
+ def _validate_callback_output(self, output_value, output):
+ valid = [str, dict, int, float, type(None), Component]
+
+ def _raise_invalid(bad_val, outer_val, bad_type, path, index=None,
+ toplevel=False):
+ outer_id = "(id={:s})".format(outer_val.id) \
+ if getattr(outer_val, 'id', False) else ''
+ outer_type = type(outer_val).__name__
+ raise exceptions.InvalidCallbackReturnValue('''
+ The callback for property `{property:s}` of component `{id:s}`
+ returned a {object:s} having type `{type:s}`
+ which is not JSON serializable.
+
+ {location_header:s}{location:s}
+ and has string representation
+ `{bad_val}`
+
+ In general, Dash properties can only be
+ dash components, strings, dictionaries, numbers, None,
+ or lists of those.
+ '''.format(
+ property=output.component_property,
+ id=output.component_id,
+ object='tree with one value' if not toplevel else 'value',
+ type=bad_type,
+ location_header=(
+ 'The value in question is located at'
+ if not toplevel else
+ '''The value in question is either the only value returned,
+ or is in the top level of the returned list,'''
+ ),
+ location=(
+ "\n" +
+ ("[{:d}] {:s} {:s}".format(index, outer_type, outer_id)
+ if index is not None
+ else ('[*] ' + outer_type + ' ' + outer_id))
+ + "\n" + path + "\n"
+ ) if not toplevel else '',
+ bad_val=bad_val).replace(' ', ''))
+
+ def _value_is_valid(val):
+ return (
+ # pylint: disable=unused-variable
+ any([isinstance(val, x) for x in valid]) or
+ type(val).__name__ == 'unicode'
+ )
+
+ def _validate_value(val, index=None):
+ # val is a Component
+ if isinstance(val, Component):
+ for p, j in val.traverse_with_paths():
+ # check each component value in the tree
+ if not _value_is_valid(j):
+ _raise_invalid(
+ bad_val=j,
+ outer_val=val,
+ bad_type=type(j).__name__,
+ path=p,
+ index=index
+ )
+
+ # Children that are not of type Component or
+ # collections.MutableSequence not returned by traverse
+ child = getattr(j, 'children', None)
+ if not isinstance(child, collections.MutableSequence):
+ if child and not _value_is_valid(child):
+ _raise_invalid(
+ bad_val=child,
+ outer_val=val,
+ bad_type=type(child).__name__,
+ path=p + "\n" + "[*] " + type(child).__name__,
+ index=index
+ )
+
+ # Also check the child of val, as it will not be returned
+ child = getattr(val, 'children', None)
+ if not isinstance(child, collections.MutableSequence):
+ if child and not _value_is_valid(child):
+ _raise_invalid(
+ bad_val=child,
+ outer_val=val,
+ bad_type=type(child).__name__,
+ path=type(child).__name__,
+ index=index
+ )
+
+ # val is not a Component, but is at the top level of tree
+ else:
+ if not _value_is_valid(val):
+ _raise_invalid(
+ bad_val=val,
+ outer_val=type(val).__name__,
+ bad_type=type(val).__name__,
+ path='',
+ index=index,
+ toplevel=True
+ )
+
+ if isinstance(output_value, list):
+ for i, val in enumerate(output_value):
+ _validate_value(val, index=i)
+ else:
+ _validate_value(output_value)
+
+ # TODO - Update nomenclature.
+ # "Parents" and "Children" should refer to the DOM tree
+ # and not the dependency tree.
+ # The dependency tree should use the nomenclature
+ # "observer" and "controller".
+ # "observers" listen for changes from their "controllers". For example,
+ # if a graph depends on a dropdown, the graph is the "observer" and the
+ # dropdown is a "controller". In this case the graph's "dependency" is
+ # the dropdown.
+ # TODO - Check this map for recursive or other ill-defined non-tree
+ # relationships
+ # pylint: disable=dangerous-default-value
+ def callback(self, output, inputs=[], state=[], events=[]):
+ self._validate_callback(output, inputs, state, events)
+
+ callback_id = '{}.{}'.format(
+ output.component_id, output.component_property
+ )
+ self.callback_map[callback_id] = {
+ 'inputs': [
+ {'id': c.component_id, 'property': c.component_property}
+ for c in inputs
+ ],
+ 'state': [
+ {'id': c.component_id, 'property': c.component_property}
+ for c in state
+ ],
+ 'events': [
+ {'id': c.component_id, 'event': c.component_event}
+ for c in events
+ ]
+ }
+
+ def wrap_func(func):
+ @wraps(func)
+ def add_context(*args, **kwargs):
+ output_value = func(*args, **kwargs)
+ response = {
+ 'response': {
+ 'props': {
+ output.component_property: output_value
+ }
+ }
+ }
+
+ try:
+ return JsonResponse(response)
+ except TypeError:
+ self._validate_callback_output(output_value, output)
+ raise exceptions.InvalidCallbackReturnValue('''
+ The callback for property `{property:s}`
+ of component `{id:s}` returned a value
+ which is not JSON serializable.
+
+ In general, Dash properties can only be
+ dash components, strings, dictionaries, numbers, None,
+ or lists of those.
+ '''.format(property=output.component_property,
+ id=output.component_id))
+
+ self.callback_map[callback_id]['callback'] = add_context
+
+ return add_context
+
+ return wrap_func
+
+ def update_component(self, output, inputs, state, **kwargs):
+ target_id = '{}.{}'.format(output['id'], output['property'])
+ args = []
+ for component_registration in self.callback_map[target_id]['inputs']:
+ args.append([
+ c.get('value', None) for c in inputs if
+ c['property'] == component_registration['property'] and
+ c['id'] == component_registration['id']
+ ][0])
+
+ for component_registration in self.callback_map[target_id]['state']:
+ args.append([
+ c.get('value', None) for c in state if
+ c['property'] == component_registration['property'] and
+ c['id'] == component_registration['id']
+ ][0])
+
+ return self.callback_map[target_id]['callback'](*args, **kwargs)
+
+ def _validate_layout(self):
+ if self.layout is None:
+ raise exceptions.NoLayoutException(
+ ''
+ 'The layout was `None` '
+ 'at the time that `run_server` was called. '
+ 'Make sure to set the `layout` attribute of your application '
+ 'before running the server.')
+
+ to_validate = self._layout_value()
+
+ layout_id = getattr(self.layout, 'id', None)
+
+ component_ids = {layout_id} if layout_id else set()
+ for component in to_validate.traverse():
+ component_id = getattr(component, 'id', None)
+ if component_id and component_id in component_ids:
+ raise exceptions.DuplicateIdError(
+ 'Duplicate component id found'
+ ' in the initial layout: `{}`'.format(component_id))
+ component_ids.add(component_id)
+
+ def _walk_assets_directory(self):
+ ignore_filter = [self.assets_ignore] if self.assets_ignore else None
+
+ def add_resource(p, filepath):
+ res = {'asset_path': p, 'filepath': filepath}
+ if self.config.assets_external_path:
+ res['external_url'] = '{}{}'.format(self.config.assets_external_path, path)
+ return res
+
+ files = list(get_files(staticfiles_storage, ignore_patterns=ignore_filter, location=self._assets_folder))
+ for f in sorted(files):
+ path = staticfiles_storage.url(f)
+ full = staticfiles_storage.path(f)
+
+ if f.endswith('js'):
+ self.scripts.append_script(add_resource(path, full))
+ elif f.endswith('css'):
+ self.css.append_css(add_resource(path, full))
+ elif f.endswith('favicon.ico'):
+ self._favicon = path
+
+
+class BaseDashView(six.with_metaclass(MetaDashView, View)):
+ dash_template = None
+ dash_base_url = '/'
+ dash_name = None
+ dash_meta_tags = None
+ dash_external_scripts = None
+ dash_external_stylesheets = None
+ dash_assets_folder = None
+ dash_assets_ignore = None
+ dash_prefix = '' # For additional special urls
+ _dashes = {}
+
+ def __init__(self, **kwargs):
+ dash_base_url = kwargs.pop('dash_base_url', self.dash_base_url)
+ dash_template = kwargs.pop('dash_template', self.dash_template)
+ dash_meta_tags = kwargs.pop('dash_meta_tags', self.dash_meta_tags)
+ dash_external_scripts = kwargs.pop('dash_external_scripts', self.dash_external_scripts)
+ dash_external_stylesheets = kwargs.pop('dash_external_stylesheets', self.dash_external_stylesheets)
+ dash_assets_folder = kwargs.pop('dash_assets_folder', self.dash_assets_folder)
+ dash_assets_ignore = kwargs.pop('dash_assets_ignore', self.dash_assets_ignore)
+
+ super(BaseDashView, self).__init__(**kwargs)
+
+ dash = getattr(self, 'dash', None)
+ if not isinstance(dash, Dash):
+ self.dash = Dash()
+
+ if dash_base_url and self.dash.url_base_pathname != dash_base_url:
+ self.dash.url_base_pathname = dash_base_url # pylint: disable=access-member-before-definition
+ # pylint: disable=access-member-before-definition
+ self.dash.config.requests_pathname_prefix = dash_base_url
+
+ if dash_template and self.dash.index_string != dash_template:
+ self.dash.index_string = dash_template
+ if dash_meta_tags and self.dash._meta_tags != dash_meta_tags: # pylint: disable=protected-access
+ # pylint: disable=protected-access, access-member-before-definition
+ self.dash._meta_tags = dash_meta_tags
+ # pylint: disable=protected-access
+ if dash_external_scripts and self.dash._external_scripts != dash_external_scripts:
+ # pylint: disable=protected-access, access-member-before-definition
+ self.dash._external_scripts = dash_external_scripts
+ # pylint: disable=protected-access
+ if dash_external_stylesheets and self.dash._external_stylesheets != dash_external_stylesheets:
+ # pylint: disable=protected-access, access-member-before-definition
+ self.dash._external_stylesheets = dash_external_stylesheets
+ # pylint: disable=protected-access
+ if dash_assets_folder and self.dash._assets_folder != dash_assets_folder:
+ # pylint: disable=protected-access, access-member-before-definition
+ self.dash._assets_folder = dash_assets_folder
+ if dash_assets_ignore and self.dash.assets_ignore != dash_assets_ignore:
+ self.dash.assets_ignore = dash_assets_ignore
+
+ @staticmethod
+ def _dash_base_url(path, part):
+ return path[:path.find(part) + 1]
+
+ def _dash_index(self, request, *args, **kwargs): # pylint: disable=unused-argument
+ return HttpResponse(self.dash.index())
+
+ def _dash_dependencies(self, request, *args, **kwargs): # pylint: disable=unused-argument
+ return JsonResponse(self.dash.dependencies())
+
+ def _dash_layout(self, request, *args, **kwargs): # pylint: disable=unused-argument
+ # TODO - Set browser cache limit - pass hash into frontend
+ return JsonResponse(self.dash._layout_value()) # pylint: disable=protected-access
+
+ def _dash_upd_component(self, request, *args, **kwargs): # pylint: disable=unused-argument
+ body = json.loads(request.body)
+
+ output = body['output']
+ inputs = body.get('inputs', [])
+ state = body.get('state', [])
+
+ return self.dash.update_component(output, inputs, state)
+
+ def _dash_component_suites(self, request, *args, **kwargs): # pylint: disable=unused-argument
+ ext = kwargs.get('path_in_package_dist', '').split('.')[-1]
+ mimetype = {
+ 'js': 'application/JavaScript',
+ 'css': 'text/css'
+ }[ext]
+
+ response = HttpResponse(self.dash.serve_component_suites(*args, **kwargs), content_type=mimetype)
+ response['Cache-Control'] = 'public, max-age={}'.format(self.dash.config.components_cache_max_age)
+
+ return response
+
+ def _dash_routes(self, request, *args, **kwargs): # pylint: disable=unused-argument
+ return JsonResponse(self.dash.serve_routes(*args, **kwargs))
+
+ @classmethod
+ def serve_dash_index(cls, request, dash_name, *args, **kwargs):
+ view = cls._dashes[dash_name](dash_base_url=request.path)
+ return view._dash_index(request, *args, **kwargs) # pylint: disable=protected-access
+
+ @classmethod
+ def serve_dash_dependencies(cls, request, dash_name, *args, **kwargs):
+ view = cls._dashes[dash_name](dash_base_url=cls._dash_base_url(request.path, '/_dash-dependencies'))
+ return view._dash_dependencies(request, *args, **kwargs) # pylint: disable=protected-access
+
+ @classmethod
+ def serve_dash_layout(cls, request, dash_name, *args, **kwargs):
+ view = cls._dashes[dash_name](dash_base_url=cls._dash_base_url(request.path, '/_dash-layout'))
+ return view._dash_layout(request, *args, **kwargs) # pylint: disable=protected-access
+
+ @classmethod
+ @csrf_exempt
+ def serve_dash_upd_component(cls, request, dash_name, *args, **kwargs):
+ view = cls._dashes[dash_name](dash_base_url=cls._dash_base_url(request.path, '/_dash-update-component'))
+ return view._dash_upd_component(request, *args, **kwargs) # pylint: disable=protected-access
+
+ @classmethod
+ def serve_dash_component_suites(cls, request, dash_name, *args, **kwargs):
+ view = cls._dashes[dash_name](dash_base_url=cls._dash_base_url(request.path, '/_dash-component-suites'))
+ return view._dash_component_suites(request, *args, **kwargs) # pylint: disable=protected-access
+
+ @classmethod
+ def serve_dash_routes(cls, request, dash_name, *args, **kwargs):
+ view = cls._dashes[dash_name](dash_base_url=cls._dash_base_url(request.path, '/_dash-routes'))
+ return view._dash_component_suites(request, *args, **kwargs) # pylint: disable=protected-access
diff --git a/dash/dependencies.py b/dash/dependencies.py
new file mode 100644
index 0000000..0421860
--- /dev/null
+++ b/dash/dependencies.py
@@ -0,0 +1,26 @@
+# pylint: disable=old-style-class, too-few-public-methods
+class Output:
+ def __init__(self, component_id, component_property):
+ self.component_id = component_id
+ self.component_property = component_property
+
+
+# pylint: disable=old-style-class, too-few-public-methods
+class Input:
+ def __init__(self, component_id, component_property):
+ self.component_id = component_id
+ self.component_property = component_property
+
+
+# pylint: disable=old-style-class, too-few-public-methods
+class State:
+ def __init__(self, component_id, component_property):
+ self.component_id = component_id
+ self.component_property = component_property
+
+
+# pylint: disable=old-style-class, too-few-public-methods
+class Event:
+ def __init__(self, component_id, component_event):
+ self.component_id = component_id
+ self.component_event = component_event
diff --git a/dash/development/__init__.py b/dash/development/__init__.py
new file mode 100644
index 0000000..a8c6749
--- /dev/null
+++ b/dash/development/__init__.py
@@ -0,0 +1,2 @@
+from . import base_component # noqa:F401
+from . import component_loader # noqa:F401
diff --git a/dash/development/base_component.py b/dash/development/base_component.py
new file mode 100644
index 0000000..b3bd6f7
--- /dev/null
+++ b/dash/development/base_component.py
@@ -0,0 +1,839 @@
+import collections
+import copy
+import os
+import inspect
+import keyword
+
+
+def is_number(s):
+ try:
+ float(s)
+ return True
+ except ValueError:
+ return False
+
+
+def _check_if_has_indexable_children(item):
+ if (not hasattr(item, 'children') or
+ (not isinstance(item.children, Component) and
+ not isinstance(item.children, collections.MutableSequence))):
+
+ raise KeyError
+
+
+def _explicitize_args(func):
+ # Python 2
+ if hasattr(func, 'func_code'):
+ varnames = func.func_code.co_varnames
+ # Python 3
+ else:
+ varnames = func.__code__.co_varnames
+
+ def wrapper(*args, **kwargs):
+ if '_explicit_args' in kwargs.keys():
+ raise Exception('Variable _explicit_args should not be set.')
+ kwargs['_explicit_args'] = \
+ list(
+ set(
+ list(varnames[:len(args)]) + [k for k, _ in kwargs.items()]
+ )
+ )
+ if 'self' in kwargs['_explicit_args']:
+ kwargs['_explicit_args'].remove('self')
+ return func(*args, **kwargs)
+
+ # If Python 3, we can set the function signature to be correct
+ if hasattr(inspect, 'signature'):
+ # pylint: disable=no-member
+ new_sig = inspect.signature(wrapper).replace(
+ parameters=inspect.signature(func).parameters.values()
+ )
+ wrapper.__signature__ = new_sig
+ return wrapper
+
+
+class Component(collections.MutableMapping):
+ class _UNDEFINED(object):
+ def __repr__(self):
+ return 'undefined'
+
+ def __str__(self):
+ return 'undefined'
+
+ UNDEFINED = _UNDEFINED()
+
+ class _REQUIRED(object):
+ def __repr__(self):
+ return 'required'
+
+ def __str__(self):
+ return 'required'
+
+ REQUIRED = _REQUIRED()
+
+ def __init__(self, **kwargs):
+ # pylint: disable=super-init-not-called
+ for k, v in list(kwargs.items()):
+ # pylint: disable=no-member
+ k_in_propnames = k in self._prop_names
+ k_in_wildcards = any([k.startswith(w)
+ for w in
+ self._valid_wildcard_attributes])
+ if not k_in_propnames and not k_in_wildcards:
+ raise TypeError(
+ 'Unexpected keyword argument `{}`'.format(k) +
+ '\nAllowed arguments: {}'.format(
+ # pylint: disable=no-member
+ ', '.join(sorted(self._prop_names))
+ )
+ )
+ setattr(self, k, v)
+
+ def to_plotly_json(self):
+ # Add normal properties
+ props = {
+ p: getattr(self, p)
+ for p in self._prop_names # pylint: disable=no-member
+ if hasattr(self, p)
+ }
+ # Add the wildcard properties data-* and aria-*
+ props.update({
+ k: getattr(self, k)
+ for k in self.__dict__
+ if any(k.startswith(w) for w in
+ self._valid_wildcard_attributes) # pylint:disable=no-member
+ })
+ as_json = {
+ 'props': props,
+ 'type': self._type, # pylint: disable=no-member
+ 'namespace': self._namespace # pylint: disable=no-member
+ }
+
+ return as_json
+
+ # pylint: disable=too-many-branches, too-many-return-statements
+ # pylint: disable=redefined-builtin, inconsistent-return-statements
+ def _get_set_or_delete(self, id, operation, new_item=None):
+ _check_if_has_indexable_children(self)
+
+ # pylint: disable=access-member-before-definition,
+ # pylint: disable=attribute-defined-outside-init
+ if isinstance(self.children, Component):
+ if getattr(self.children, 'id', None) is not None:
+ # Woohoo! It's the item that we're looking for
+ if self.children.id == id:
+ if operation == 'get': # pylint: disable=no-else-return
+ return self.children
+ elif operation == 'set':
+ self.children = new_item
+ return
+ elif operation == 'delete':
+ self.children = None
+ return
+
+ # Recursively dig into its subtree
+ try:
+ if operation == 'get': # pylint: disable=no-else-return
+ return self.children.__getitem__(id)
+ elif operation == 'set':
+ self.children.__setitem__(id, new_item)
+ return
+ elif operation == 'delete':
+ self.children.__delitem__(id)
+ return
+ except KeyError:
+ pass
+
+ # if children is like a list
+ if isinstance(self.children, collections.MutableSequence):
+ for i, item in enumerate(self.children):
+ # If the item itself is the one we're looking for
+ if getattr(item, 'id', None) == id: # pylint: disable=no-else-return
+ if operation == 'get': # pylint: disable=no-else-return
+ return item
+ elif operation == 'set':
+ self.children[i] = new_item
+ return
+ elif operation == 'delete':
+ del self.children[i]
+ return
+
+ # Otherwise, recursively dig into that item's subtree
+ # Make sure it's not like a string
+ elif isinstance(item, Component):
+ try:
+ if operation == 'get': # pylint: disable=no-else-return
+ return item.__getitem__(id)
+ elif operation == 'set':
+ item.__setitem__(id, new_item)
+ return
+ elif operation == 'delete':
+ item.__delitem__(id)
+ return
+ except KeyError:
+ pass
+
+ # The end of our branch
+ # If we were in a list, then this exception will get caught
+ raise KeyError(id)
+
+ # Supply ABC methods for a MutableMapping:
+ # - __getitem__
+ # - __setitem__
+ # - __delitem__
+ # - __iter__
+ # - __len__
+
+ def __getitem__(self, id): # pylint: disable=redefined-builtin
+ """Recursively find the element with the given ID through the tree
+ of children.
+ """
+
+ # A component's children can be undefined, a string, another component,
+ # or a list of components.
+ return self._get_set_or_delete(id, 'get')
+
+ def __setitem__(self, id, item): # pylint: disable=redefined-builtin
+ """Set an element by its ID."""
+ return self._get_set_or_delete(id, 'set', item)
+
+ def __delitem__(self, id): # pylint: disable=redefined-builtin
+ """Delete items by ID in the tree of children."""
+ return self._get_set_or_delete(id, 'delete')
+
+ def traverse(self):
+ """Yield each item in the tree."""
+ for t in self.traverse_with_paths():
+ yield t[1]
+
+ def traverse_with_paths(self):
+ """Yield each item with its path in the tree."""
+ children = getattr(self, 'children', None)
+ children_type = type(children).__name__
+ children_id = "(id={:s})".format(children.id) \
+ if getattr(children, 'id', False) else ''
+ children_string = children_type + ' ' + children_id
+
+ # children is just a component
+ if isinstance(children, Component):
+ yield "[*] " + children_string, children
+ for p, t in children.traverse_with_paths():
+ yield "\n".join(["[*] " + children_string, p]), t
+
+ # children is a list of components
+ elif isinstance(children, collections.MutableSequence):
+ for idx, i in enumerate(children):
+ list_path = "[{:d}] {:s} {}".format(
+ idx,
+ type(i).__name__,
+ "(id={:s})".format(i.id) if getattr(i, 'id', False) else ''
+ )
+ yield list_path, i
+
+ if isinstance(i, Component):
+ for p, t in i.traverse_with_paths():
+ yield "\n".join([list_path, p]), t
+
+ def __iter__(self):
+ """Yield IDs in the tree of children."""
+ for t in self.traverse():
+ if (isinstance(t, Component) and
+ getattr(t, 'id', None) is not None):
+
+ yield t.id
+
+ def __len__(self):
+ """Return the number of items in the tree."""
+ # TODO - Should we return the number of items that have IDs
+ # or just the number of items?
+ # The number of items is more intuitive but returning the number
+ # of IDs matches __iter__ better.
+ length = 0
+ if getattr(self, 'children', None) is None:
+ length = 0
+ elif isinstance(self.children, Component):
+ length = 1
+ length += len(self.children)
+ elif isinstance(self.children, collections.MutableSequence):
+ for c in self.children:
+ length += 1
+ if isinstance(c, Component):
+ length += len(c)
+ else:
+ # string or number
+ length = 1
+ return length
+
+
+# pylint: disable=unused-argument
+def generate_class_string(typename, props, description, namespace):
+ """
+ Dynamically generate class strings to have nicely formatted docstrings,
+ keyword arguments, and repr
+
+ Inspired by http://jameso.be/2013/08/06/namedtuple.html
+
+ Parameters
+ ----------
+ typename
+ props
+ description
+ namespace
+
+ Returns
+ -------
+ string
+
+ """
+ # TODO _prop_names, _type, _namespace, available_events,
+ # and available_properties
+ # can be modified by a Dash JS developer via setattr
+ # TODO - Tab out the repr for the repr of these components to make it
+ # look more like a hierarchical tree
+ # TODO - Include "description" "defaultValue" in the repr and docstring
+ #
+ # TODO - Handle "required"
+ #
+ # TODO - How to handle user-given `null` values? I want to include
+ # an expanded docstring like Dropdown(value=None, id=None)
+ # but by templating in those None values, I have no way of knowing
+ # whether a property is None because the user explicitly wanted
+ # it to be `null` or whether that was just the default value.
+ # The solution might be to deal with default values better although
+ # not all component authors will supply those.
+ c = '''class {typename}(Component):
+ """{docstring}"""
+ @_explicitize_args
+ def __init__(self, {default_argtext}):
+ self._prop_names = {list_of_valid_keys}
+ self._type = '{typename}'
+ self._namespace = '{namespace}'
+ self._valid_wildcard_attributes =\
+ {list_of_valid_wildcard_attr_prefixes}
+ self.available_events = {events}
+ self.available_properties = {list_of_valid_keys}
+ self.available_wildcard_properties =\
+ {list_of_valid_wildcard_attr_prefixes}
+
+ _explicit_args = kwargs.pop('_explicit_args')
+ _locals = locals()
+ _locals.update(kwargs) # For wildcard attrs
+ args = {{k: _locals[k] for k in _explicit_args if k != 'children'}}
+
+ for k in {required_args}:
+ if k not in args:
+ raise TypeError(
+ 'Required argument `' + k + '` was not specified.')
+ super({typename}, self).__init__({argtext})
+
+ def __repr__(self):
+ if(any(getattr(self, c, None) is not None
+ for c in self._prop_names
+ if c is not self._prop_names[0])
+ or any(getattr(self, c, None) is not None
+ for c in self.__dict__.keys()
+ if any(c.startswith(wc_attr)
+ for wc_attr in self._valid_wildcard_attributes))):
+ props_string = ', '.join([c+'='+repr(getattr(self, c, None))
+ for c in self._prop_names
+ if getattr(self, c, None) is not None])
+ wilds_string = ', '.join([c+'='+repr(getattr(self, c, None))
+ for c in self.__dict__.keys()
+ if any([c.startswith(wc_attr)
+ for wc_attr in
+ self._valid_wildcard_attributes])])
+ return ('{typename}(' + props_string +
+ (', ' + wilds_string if wilds_string != '' else '') + ')')
+ else:
+ return (
+ '{typename}(' +
+ repr(getattr(self, self._prop_names[0], None)) + ')')
+'''
+
+ filtered_props = reorder_props(filter_props(props))
+ # pylint: disable=unused-variable
+ list_of_valid_wildcard_attr_prefixes = repr(parse_wildcards(props))
+ # pylint: disable=unused-variable
+ list_of_valid_keys = repr(list(map(str, filtered_props.keys())))
+ # pylint: disable=unused-variable
+ docstring = create_docstring(
+ component_name=typename,
+ props=filtered_props,
+ events=parse_events(props),
+ description=description)
+
+ # pylint: disable=unused-variable
+ events = '[' + ', '.join(parse_events(props)) + ']'
+ prop_keys = list(props.keys())
+ if 'children' in props:
+ prop_keys.remove('children')
+ default_argtext = "children=None, "
+ # pylint: disable=unused-variable
+ argtext = 'children=children, **args'
+ else:
+ default_argtext = ""
+ argtext = '**args'
+ default_argtext += ", ".join(
+ [('{:s}=Component.REQUIRED'.format(p)
+ if props[p]['required'] else
+ '{:s}=Component.UNDEFINED'.format(p))
+ for p in prop_keys
+ if not p.endswith("-*") and
+ p not in keyword.kwlist and
+ p not in ['dashEvents', 'fireEvent', 'setProps']] + ['**kwargs']
+ )
+
+ required_args = required_props(props)
+ return c.format(**locals())
+
+
+# pylint: disable=unused-argument
+def generate_class_file(typename, props, description, namespace):
+ """
+ Generate a python class file (.py) given a class string
+
+ Parameters
+ ----------
+ typename
+ props
+ description
+ namespace
+
+ Returns
+ -------
+
+ """
+ import_string =\
+ "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \
+ "from dash.development.base_component import " + \
+ "Component, _explicitize_args\n\n\n"
+ class_string = generate_class_string(
+ typename,
+ props,
+ description,
+ namespace
+ )
+ file_name = "{:s}.py".format(typename)
+
+ file_path = os.path.join(namespace, file_name)
+ with open(file_path, 'w') as f:
+ f.write(import_string)
+ f.write(class_string)
+
+
+# pylint: disable=unused-argument
+def generate_class(typename, props, description, namespace):
+ """
+ Generate a python class object given a class string
+
+ Parameters
+ ----------
+ typename
+ props
+ description
+ namespace
+
+ Returns
+ -------
+
+ """
+ string = generate_class_string(typename, props, description, namespace)
+ scope = {'Component': Component, '_explicitize_args': _explicitize_args}
+ # pylint: disable=exec-used
+ exec(string, scope)
+ result = scope[typename]
+ return result
+
+
+def required_props(props):
+ """
+ Pull names of required props from the props object
+
+ Parameters
+ ----------
+ props: dict
+
+ Returns
+ -------
+ list
+ List of prop names (str) that are required for the Component
+ """
+ return [prop_name for prop_name, prop in list(props.items())
+ if prop['required']]
+
+
+def create_docstring(component_name, props, events, description):
+ """
+ Create the Dash component docstring
+
+ Parameters
+ ----------
+ component_name: str
+ Component name
+ props: dict
+ Dictionary with {propName: propMetadata} structure
+ events: list
+ List of Dash events
+ description: str
+ Component description
+
+ Returns
+ -------
+ str
+ Dash component docstring
+ """
+ # Ensure props are ordered with children first
+ props = reorder_props(props=props)
+
+ return (
+ """A {name} component.\n{description}
+
+Keyword arguments:\n{args}
+
+Available events: {events}"""
+ ).format(
+ name=component_name,
+ description=description,
+ args='\n'.join(
+ create_prop_docstring(
+ prop_name=p,
+ type_object=prop['type'] if 'type' in prop
+ else prop['flowType'],
+ required=prop['required'],
+ description=prop['description'],
+ indent_num=0,
+ is_flow_type='flowType' in prop and 'type' not in prop)
+ for p, prop in list(filter_props(props).items())),
+ events=', '.join(events))
+
+
+def parse_events(props):
+ """
+ Pull out the dashEvents from the Component props
+
+ Parameters
+ ----------
+ props: dict
+ Dictionary with {propName: propMetadata} structure
+
+ Returns
+ -------
+ list
+ List of Dash event strings
+ """
+ if 'dashEvents' in props and props['dashEvents']['type']['name'] == 'enum':
+ events = [v['value'] for v in props['dashEvents']['type']['value']]
+ else:
+ events = []
+
+ return events
+
+
+def parse_wildcards(props):
+ """
+ Pull out the wildcard attributes from the Component props
+
+ Parameters
+ ----------
+ props: dict
+ Dictionary with {propName: propMetadata} structure
+
+ Returns
+ -------
+ list
+ List of Dash valid wildcard prefixes
+ """
+ list_of_valid_wildcard_attr_prefixes = []
+ for wildcard_attr in ["data-*", "aria-*"]:
+ if wildcard_attr in props.keys():
+ list_of_valid_wildcard_attr_prefixes.append(wildcard_attr[:-1])
+ return list_of_valid_wildcard_attr_prefixes
+
+
+def reorder_props(props):
+ """
+ If "children" is in props, then move it to the
+ front to respect dash convention
+
+ Parameters
+ ----------
+ props: dict
+ Dictionary with {propName: propMetadata} structure
+
+ Returns
+ -------
+ dict
+ Dictionary with {propName: propMetadata} structure
+ """
+ if 'children' in props:
+ props = collections.OrderedDict(
+ [('children', props.pop('children'),)] +
+ list(zip(list(props.keys()), list(props.values()))))
+
+ return props
+
+
+def filter_props(props):
+ """
+ Filter props from the Component arguments to exclude:
+ - Those without a "type" or a "flowType" field
+ - Those with arg.type.name in {'func', 'symbol', 'instanceOf'}
+ - dashEvents as a name
+
+ Parameters
+ ----------
+ props: dict
+ Dictionary with {propName: propMetadata} structure
+
+ Returns
+ -------
+ dict
+ Filtered dictionary with {propName: propMetadata} structure
+
+ Examples
+ --------
+ ```python
+ prop_args = {
+ 'prop1': {
+ 'type': {'name': 'bool'},
+ 'required': False,
+ 'description': 'A description',
+ 'flowType': {},
+ 'defaultValue': {'value': 'false', 'computed': False},
+ },
+ 'prop2': {'description': 'A prop without a type'},
+ 'prop3': {
+ 'type': {'name': 'func'},
+ 'description': 'A function prop',
+ },
+ }
+ # filtered_prop_args is now
+ # {
+ # 'prop1': {
+ # 'type': {'name': 'bool'},
+ # 'required': False,
+ # 'description': 'A description',
+ # 'flowType': {},
+ # 'defaultValue': {'value': 'false', 'computed': False},
+ # },
+ # }
+ filtered_prop_args = filter_props(prop_args)
+ ```
+ """
+ filtered_props = copy.deepcopy(props)
+
+ for arg_name, arg in list(filtered_props.items()):
+ if 'type' not in arg and 'flowType' not in arg:
+ filtered_props.pop(arg_name)
+ continue
+
+ # Filter out functions and instances --
+ # these cannot be passed from Python
+ if 'type' in arg: # These come from PropTypes
+ arg_type = arg['type']['name']
+ if arg_type in {'func', 'symbol', 'instanceOf'}:
+ filtered_props.pop(arg_name)
+ elif 'flowType' in arg: # These come from Flow & handled differently
+ arg_type_name = arg['flowType']['name']
+ if arg_type_name == 'signature':
+ # This does the same as the PropTypes filter above, but "func"
+ # is under "type" if "name" is "signature" vs just in "name"
+ if 'type' not in arg['flowType'] \
+ or arg['flowType']['type'] != 'object':
+ filtered_props.pop(arg_name)
+ else:
+ raise ValueError
+
+ # dashEvents are a special oneOf property that is used for subscribing
+ # to events but it's never set as a property
+ if arg_name in ['dashEvents']:
+ filtered_props.pop(arg_name)
+ return filtered_props
+
+
+# pylint: disable=too-many-arguments
+def create_prop_docstring(prop_name, type_object, required, description,
+ indent_num, is_flow_type=False):
+ """
+ Create the Dash component prop docstring
+
+ Parameters
+ ----------
+ prop_name: str
+ Name of the Dash component prop
+ type_object: dict
+ react-docgen-generated prop type dictionary
+ required: bool
+ Component is required?
+ description: str
+ Dash component description
+ indent_num: int
+ Number of indents to use for the context block
+ (creates 2 spaces for every indent)
+ is_flow_type: bool
+ Does the prop use Flow types? Otherwise, uses PropTypes
+
+ Returns
+ -------
+ str
+ Dash component prop docstring
+ """
+ py_type_name = js_to_py_type(
+ type_object=type_object,
+ is_flow_type=is_flow_type,
+ indent_num=indent_num + 1)
+
+ indent_spacing = ' ' * indent_num
+ if '\n' in py_type_name:
+ return '{indent_spacing}- {name} ({is_required}): {description}. ' \
+ '{name} has the following type: {type}'.format(
+ indent_spacing=indent_spacing,
+ name=prop_name,
+ type=py_type_name,
+ description=description,
+ is_required='required' if required else 'optional')
+ return '{indent_spacing}- {name} ({type}' \
+ '{is_required}){description}'.format(
+ indent_spacing=indent_spacing,
+ name=prop_name,
+ type='{}; '.format(py_type_name) if py_type_name else '',
+ description=(
+ ': {}'.format(description) if description != '' else ''
+ ),
+ is_required='required' if required else 'optional')
+
+
+def map_js_to_py_types_prop_types(type_object):
+ """Mapping from the PropTypes js type object to the Python type"""
+ return dict(
+ array=lambda: 'list',
+ bool=lambda: 'boolean',
+ number=lambda: 'number',
+ string=lambda: 'string',
+ object=lambda: 'dict',
+ any=lambda: 'boolean | number | string | dict | list',
+ element=lambda: 'dash component',
+ node=lambda: 'a list of or a singular dash '
+ 'component, string or number',
+
+ # React's PropTypes.oneOf
+ enum=lambda: 'a value equal to: {}'.format(
+ ', '.join(
+ '{}'.format(str(t['value']))
+ for t in type_object['value'])),
+
+ # React's PropTypes.oneOfType
+ union=lambda: '{}'.format(
+ ' | '.join(
+ '{}'.format(js_to_py_type(subType))
+ for subType in type_object['value']
+ if js_to_py_type(subType) != '')),
+
+ # React's PropTypes.arrayOf
+ arrayOf=lambda: 'list'.format( # pylint: disable=too-many-format-args
+ ' of {}s'.format(
+ js_to_py_type(type_object['value']))
+ if js_to_py_type(type_object['value']) != ''
+ else ''),
+
+ # React's PropTypes.objectOf
+ objectOf=lambda: (
+ 'dict with strings as keys and values of type {}'
+ ).format(
+ js_to_py_type(type_object['value'])),
+
+ # React's PropTypes.shape
+ shape=lambda: 'dict containing keys {}.\n{}'.format(
+ ', '.join(
+ "'{}'".format(t)
+ for t in list(type_object['value'].keys())),
+ 'Those keys have the following types: \n{}'.format(
+ '\n'.join(create_prop_docstring(
+ prop_name=prop_name,
+ type_object=prop,
+ required=prop['required'],
+ description=prop.get('description', ''),
+ indent_num=1)
+ for prop_name, prop in
+ list(type_object['value'].items())))),
+ )
+
+
+def map_js_to_py_types_flow_types(type_object):
+ """Mapping from the Flow js types to the Python type"""
+ return dict(
+ array=lambda: 'list',
+ boolean=lambda: 'boolean',
+ number=lambda: 'number',
+ string=lambda: 'string',
+ Object=lambda: 'dict',
+ any=lambda: 'bool | number | str | dict | list',
+ Element=lambda: 'dash component',
+ Node=lambda: 'a list of or a singular dash '
+ 'component, string or number',
+
+ # React's PropTypes.oneOfType
+ union=lambda: '{}'.format(
+ ' | '.join(
+ '{}'.format(js_to_py_type(subType))
+ for subType in type_object['elements']
+ if js_to_py_type(subType) != '')),
+
+ # Flow's Array type
+ Array=lambda: 'list{}'.format(
+ ' of {}s'.format(
+ js_to_py_type(type_object['elements'][0]))
+ if js_to_py_type(type_object['elements'][0]) != ''
+ else ''),
+
+ # React's PropTypes.shape
+ signature=lambda indent_num: 'dict containing keys {}.\n{}'.format(
+ ', '.join("'{}'".format(d['key'])
+ for d in type_object['signature']['properties']),
+ '{}Those keys have the following types: \n{}'.format(
+ ' ' * indent_num,
+ '\n'.join(
+ create_prop_docstring(
+ prop_name=prop['key'],
+ type_object=prop['value'],
+ required=prop['value']['required'],
+ description=prop['value'].get('description', ''),
+ indent_num=indent_num,
+ is_flow_type=True)
+ for prop in type_object['signature']['properties']))),
+ )
+
+
+def js_to_py_type(type_object, is_flow_type=False, indent_num=0):
+ """
+ Convert JS types to Python types for the component definition
+
+ Parameters
+ ----------
+ type_object: dict
+ react-docgen-generated prop type dictionary
+ is_flow_type: bool
+ Does the prop use Flow types? Otherwise, uses PropTypes
+ indent_num: int
+ Number of indents to use for the docstring for the prop
+
+ Returns
+ -------
+ str
+ Python type string
+ """
+ js_type_name = type_object['name']
+ js_to_py_types = map_js_to_py_types_flow_types(type_object=type_object) \
+ if is_flow_type \
+ else map_js_to_py_types_prop_types(type_object=type_object)
+
+ # pylint: disable=no-else-return
+ if 'computed' in type_object and type_object['computed'] \
+ or type_object.get('type', '') == 'function':
+ return ''
+ elif js_type_name in js_to_py_types:
+ if js_type_name == 'signature': # This is a Flow object w/ signature
+ return js_to_py_types[js_type_name](indent_num)
+ # All other types
+ return js_to_py_types[js_type_name]()
+ return ''
diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py
new file mode 100644
index 0000000..74d2e55
--- /dev/null
+++ b/dash/development/component_loader.py
@@ -0,0 +1,109 @@
+import collections
+import json
+import os
+from .base_component import generate_class
+from .base_component import generate_class_file
+
+
+def _get_metadata(metadata_path):
+ # Start processing
+ with open(metadata_path) as data_file:
+ json_string = data_file.read()
+ data = json\
+ .JSONDecoder(object_pairs_hook=collections.OrderedDict)\
+ .decode(json_string)
+ return data
+
+
+def load_components(metadata_path,
+ namespace='default_namespace'):
+ """Load React component metadata into a format Dash can parse.
+
+ Usage: load_components('../../component-suites/lib/metadata.json')
+
+ Keyword arguments:
+ metadata_path -- a path to a JSON file created by
+ [`react-docgen`](https://github.com/reactjs/react-docgen).
+
+ Returns:
+ components -- a list of component objects with keys
+ `type`, `valid_kwargs`, and `setup`.
+ """
+
+ components = []
+
+ data = _get_metadata(metadata_path)
+
+ # Iterate over each property name (which is a path to the component)
+ for componentPath in data:
+ componentData = data[componentPath]
+
+ # Extract component name from path
+ # e.g. src/components/MyControl.react.js
+ # TODO Make more robust - some folks will write .jsx and others
+ # will be on windows. Unfortunately react-docgen doesn't include
+ # the name of the component atm.
+ name = componentPath.split('/').pop().split('.')[0]
+ component = generate_class(
+ name,
+ componentData['props'],
+ componentData['description'],
+ namespace
+ )
+
+ components.append(component)
+
+ return components
+
+
+def generate_classes(namespace, metadata_path='lib/metadata.json'):
+ """Load React component metadata into a format Dash can parse,
+ then create python class files.
+
+ Usage: generate_classes()
+
+ Keyword arguments:
+ namespace -- name of the generated python package (also output dir)
+
+ metadata_path -- a path to a JSON file created by
+ [`react-docgen`](https://github.com/reactjs/react-docgen).
+
+ Returns:
+ """
+
+ data = _get_metadata(metadata_path)
+ imports_path = os.path.join(namespace, '_imports_.py')
+
+ # Make sure the file doesn't exist, as we use append write
+ if os.path.exists(imports_path):
+ os.remove(imports_path)
+
+ # Iterate over each property name (which is a path to the component)
+ for componentPath in data:
+ componentData = data[componentPath]
+
+ # Extract component name from path
+ # e.g. src/components/MyControl.react.js
+ # TODO Make more robust - some folks will write .jsx and others
+ # will be on windows. Unfortunately react-docgen doesn't include
+ # the name of the component atm.
+ name = componentPath.split('/').pop().split('.')[0]
+ generate_class_file(
+ name,
+ componentData['props'],
+ componentData['description'],
+ namespace
+ )
+
+ # Add an import statement for this component
+ with open(imports_path, 'a') as f:
+ f.write('from .{0:s} import {0:s}\n'.format(name))
+
+ # Add the __all__ value so we can import * from _imports_
+ all_imports = [p.split('/').pop().split('.')[0] for p in data]
+ with open(imports_path, 'a') as f:
+ array_string = '[\n'
+ for a in all_imports:
+ array_string += ' "{:s}",\n'.format(a)
+ array_string += ']\n'
+ f.write('\n\n__all__ = {:s}'.format(array_string))
diff --git a/dash/exceptions.py b/dash/exceptions.py
new file mode 100644
index 0000000..5ec2779
--- /dev/null
+++ b/dash/exceptions.py
@@ -0,0 +1,66 @@
+class DashException(Exception):
+ pass
+
+
+class NoLayoutException(DashException):
+ pass
+
+
+class CallbackException(DashException):
+ pass
+
+
+class NonExistantIdException(CallbackException):
+ pass
+
+
+class NonExistantPropException(CallbackException):
+ pass
+
+
+class NonExistantEventException(CallbackException):
+ pass
+
+
+class UndefinedLayoutException(CallbackException):
+ pass
+
+
+class IncorrectTypeException(CallbackException):
+ pass
+
+
+class MissingEventsException(CallbackException):
+ pass
+
+
+class LayoutIsNotDefined(CallbackException):
+ pass
+
+
+class IDsCantContainPeriods(CallbackException):
+ pass
+
+
+class CantHaveMultipleOutputs(CallbackException):
+ pass
+
+
+class PreventUpdate(CallbackException):
+ pass
+
+
+class DuplicateIdError(DashException):
+ pass
+
+
+class InvalidCallbackReturnValue(CallbackException):
+ pass
+
+
+class InvalidConfig(DashException):
+ pass
+
+
+class InvalidResourceError(DashException):
+ pass
diff --git a/dash/resources.py b/dash/resources.py
new file mode 100644
index 0000000..aa1ce87
--- /dev/null
+++ b/dash/resources.py
@@ -0,0 +1,143 @@
+from copy import copy
+import json
+import warnings
+import os
+
+from .development.base_component import Component
+
+
+# pylint: disable=old-style-class
+class Resources:
+ def __init__(self, resource_name, layout):
+ self._resources = []
+ self.resource_name = resource_name
+ self.layout = layout
+
+ def append_resource(self, resource):
+ self._resources.append(resource)
+
+ def _filter_resources(self, all_resources, dev_bundles=False):
+ filtered_resources = []
+ for s in all_resources:
+ filtered_resource = {}
+ if 'namespace' in s:
+ filtered_resource['namespace'] = s['namespace']
+ if 'external_url' in s and not self.config.serve_locally:
+ filtered_resource['external_url'] = s['external_url']
+ elif 'dev_package_path' in s and dev_bundles:
+ filtered_resource['relative_package_path'] = (
+ s['dev_package_path']
+ )
+ elif 'relative_package_path' in s:
+ filtered_resource['relative_package_path'] = (
+ s['relative_package_path']
+ )
+ elif 'absolute_path' in s:
+ filtered_resource['absolute_path'] = s['absolute_path']
+ elif 'asset_path' in s:
+ info = os.stat(s['filepath'])
+ filtered_resource['asset_path'] = s['asset_path']
+ filtered_resource['ts'] = info.st_mtime
+ elif self.config.serve_locally:
+ warnings.warn(
+ 'A local version of {} is not available'.format(
+ s['external_url']
+ )
+ )
+ continue
+ else:
+ raise Exception(
+ '{} does not have a '
+ 'relative_package_path, absolute_path, or an '
+ 'external_url.'.format(
+ json.dumps(filtered_resource)
+ )
+ )
+
+ filtered_resources.append(filtered_resource)
+
+ return filtered_resources
+
+ def get_all_resources(self, dev_bundles=False):
+ all_resources = []
+ if self.config.infer_from_layout:
+ all_resources = (
+ self.get_inferred_resources() + self._resources
+ )
+ else:
+ all_resources = self._resources
+
+ return self._filter_resources(all_resources, dev_bundles)
+
+ def get_inferred_resources(self):
+ namespaces = []
+ resources = []
+ layout = self.layout
+
+ def extract_resource_from_component(component):
+ # pylint: disable=protected-access
+ if (isinstance(component, Component) and
+ component._namespace not in namespaces):
+
+ namespaces.append(component._namespace)
+
+ if hasattr(component, self.resource_name):
+
+ component_resources = copy(
+ getattr(component, self.resource_name)
+ )
+ for r in component_resources:
+ r['namespace'] = component._namespace
+ resources.extend(component_resources)
+
+ extract_resource_from_component(layout)
+ for t in layout.traverse():
+ extract_resource_from_component(t)
+ return resources
+
+
+class Css:
+ # pylint: disable=old-style-class
+ def __init__(self, layout=None):
+ self._resources = Resources('_css_dist', layout)
+ self._resources.config = self.config
+
+ def _update_layout(self, layout):
+ self._resources.layout = layout
+
+ def append_css(self, stylesheet):
+ self._resources.append_resource(stylesheet)
+
+ def get_all_css(self):
+ return self._resources.get_all_resources()
+
+ def get_inferred_css_dist(self):
+ return self._resources.get_inferred_resources()
+
+ # pylint: disable=old-style-class, no-init, too-few-public-methods
+ class config:
+ infer_from_layout = True
+ serve_locally = False
+
+
+class Scripts: # pylint: disable=old-style-class
+ def __init__(self, layout=None):
+ self._resources = Resources('_js_dist', layout)
+ self._resources.config = self.config
+
+ def _update_layout(self, layout):
+ self._resources.layout = layout
+
+ def append_script(self, script):
+ self._resources.append_resource(script)
+
+ def get_all_scripts(self, dev_bundles=False):
+ return self._resources.get_all_resources(dev_bundles)
+
+ def get_inferred_scripts(self):
+ return self._resources.get_inferred_resources()
+
+ # pylint: disable=old-style-class, no-init, too-few-public-methods
+ class config:
+ infer_from_layout = True
+ serve_locally = False
diff --git a/dash/urls.py b/dash/urls.py
new file mode 100644
index 0000000..a7a4090
--- /dev/null
+++ b/dash/urls.py
@@ -0,0 +1,18 @@
+from django.conf.urls import include, url
+
+from .dash import BaseDashView
+
+
+urlpatterns = [
+ url(r'^(?P[\-\w_0-9]+)/', include([
+ url(r'^$', BaseDashView.serve_dash_index),
+ url(r'^(?P[\-\w_.@0-9]+)/$', BaseDashView.serve_dash_index),
+ url(r'^_dash-dependencies', BaseDashView.serve_dash_dependencies),
+ url(r'^_dash-layout', BaseDashView.serve_dash_layout),
+ url(r'^_dash-update-component', BaseDashView.serve_dash_upd_component),
+ url(r'^_dash-component-suites/(?P[\-\w_@0-9]+)/'
+ r'(?P[\-\w_.@0-9]+)',
+ BaseDashView.serve_dash_component_suites),
+ url(r'^_dash-routes', BaseDashView.serve_dash_routes),
+ ]))
+]
diff --git a/dash/version.py b/dash/version.py
new file mode 100644
index 0000000..0404d81
--- /dev/null
+++ b/dash/version.py
@@ -0,0 +1 @@
+__version__ = '0.3.0'
diff --git a/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
index 0000000..5d05176
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1,12 @@
+;dash_core_components>=0.30.2
+;dash_html_components>=0.13.2
+;dash_renderer>=0.14.1
+;Django>=1.9,<2.2
+;dash_flow_example==0.0.3
+;dash-dangerously-set-inner-html
+selenium>=3.14.1
+tox>=3.4.0
+plotly>=2.0.8
+pytest-django>=3.4.3
+flake8>=3.5.0
+pylint==2.1.1
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..8b24811
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+DJANGO_SETTINGS_MODULE = project.settings
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e011cba
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+plotly>=2.0.8
+dash_core_components>=0.30.2
+dash_html_components>=0.13.2
+dash-renderer>=0.14.1
+Django>=1.9,<2.2
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..15e24a6
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,51 @@
+import io
+from setuptools import setup, find_packages
+
+main_ns = {}
+exec(open('dash/version.py').read(), main_ns) # pylint: disable=exec-used
+
+setup(
+ name='dj-plotly-dash',
+ version=main_ns['__version__'],
+ author='Sergei Pikhovkin',
+ author_email='s@pikhovkin.ru',
+ packages=find_packages(exclude=['tests*']),
+ license='MIT',
+ description=('A Python framework for building reactive web-apps. '
+ 'Developed by Plotly.'),
+ long_description=io.open('README.md', encoding='utf-8').read(),
+ install_requires=[
+ 'Django>=1.9,<2.2',
+ 'plotly>=2.0.8',
+ 'dash_renderer>=0.14.1',
+ ],
+ url='https://github.com/pikhovkin/dj-plotly-dash',
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Web Environment',
+ 'Framework :: Django',
+ 'Framework :: Django :: 1.9',
+ 'Framework :: Django :: 1.10',
+ 'Framework :: Django :: 1.11',
+ 'Framework :: Django :: 2.0',
+ 'Framework :: Django :: 2.1',
+ 'Intended Audience :: Developers',
+ 'Intended Audience :: Education',
+ 'Intended Audience :: Financial and Insurance Industry',
+ 'Intended Audience :: Healthcare Industry',
+ 'Intended Audience :: Manufacturing',
+ 'Intended Audience :: Science/Research',
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Topic :: Database :: Front-Ends',
+ 'Topic :: Office/Business :: Financial :: Spreadsheet',
+ 'Topic :: Scientific/Engineering :: Visualization',
+ 'Topic :: Software Development :: Libraries :: Application Frameworks',
+ 'Topic :: Software Development :: Widget Sets'
+ ]
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/development/TestReactComponent.react.js b/tests/development/TestReactComponent.react.js
new file mode 100644
index 0000000..5c45fed
--- /dev/null
+++ b/tests/development/TestReactComponent.react.js
@@ -0,0 +1,114 @@
+import React from 'react';
+// A react component with all of the available proptypes to run tests over
+
+/**
+ * This is a description of the component.
+ * It's multiple lines long.
+ */
+class ReactComponent extends Component {
+ render() {
+ return '';
+ }
+}
+
+ReactComponent.propTypes = {
+ /**
+ * Description of optionalArray
+ */
+ optionalArray: React.PropTypes.array,
+ optionalBool: React.PropTypes.bool,
+ optionalFunc: React.PropTypes.func,
+ optionalNumber: React.PropTypes.number,
+ optionalObject: React.PropTypes.object,
+ optionalString: React.PropTypes.string,
+ optionalSymbol: React.PropTypes.symbol,
+
+ // Anything that can be rendered: numbers, strings, elements or an array
+ // (or fragment) containing these types.
+ optionalNode: React.PropTypes.node,
+
+ // A React element.
+ optionalElement: React.PropTypes.element,
+
+ // You can also declare that a prop is an instance of a class. This uses
+ // JS's instanceof operator.
+ optionalMessage: React.PropTypes.instanceOf(Message),
+
+ // You can ensure that your prop is limited to specific values by treating
+ // it as an enum.
+ optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),
+
+ // An object that could be one of many types
+ optionalUnion: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ React.PropTypes.number,
+ React.PropTypes.instanceOf(Message)
+ ]),
+
+ // An array of a certain type
+ optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
+
+ // An object with property values of a certain type
+ optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
+
+ // An object taking on a particular shape
+ optionalObjectWithShapeAndNestedDescription: React.PropTypes.shape({
+ color: React.PropTypes.string,
+ fontSize: React.PropTypes.number,
+ /**
+ * Figure is a plotly graph object
+ */
+ figure: React.PropTypes.shape({
+ /**
+ * data is a collection of traces
+ */
+ data: React.PropTypes.arrayOf(React.PropTypes.object),
+ /**
+ * layout describes the rest of the figure
+ */
+ layout: React.PropTypes.object
+ })
+ }),
+
+ // A value of any data type
+ optionalAny: React.PropTypes.any,
+
+ customProp: function(props, propName, componentName) {
+ if (!/matchme/.test(props[propName])) {
+ return new Error(
+ 'Invalid prop `' + propName + '` supplied to' +
+ ' `' + componentName + '`. Validation failed.'
+ );
+ }
+ },
+
+ customArrayProp: React.PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
+ if (!/matchme/.test(propValue[key])) {
+ return new Error(
+ 'Invalid prop `' + propFullName + '` supplied to' +
+ ' `' + componentName + '`. Validation failed.'
+ );
+ }
+ }),
+
+ // special dash events
+
+ children: React.PropTypes.node,
+
+ id: React.PropTypes.string,
+
+
+ // dashEvents is a special prop that is used to events validation
+ dashEvents: React.PropTypes.oneOf([
+ 'restyle',
+ 'relayout',
+ 'click'
+ ])
+};
+
+ReactComponent.defaultProps = {
+ optionalNumber: 42,
+ optionalString: 'hello world'
+};
+
+export default ReactComponent;
diff --git a/tests/development/TestReactComponentRequired.react.js b/tests/development/TestReactComponentRequired.react.js
new file mode 100644
index 0000000..a08b0f0
--- /dev/null
+++ b/tests/development/TestReactComponentRequired.react.js
@@ -0,0 +1,19 @@
+import React from 'react';
+// A react component with all of the available proptypes to run tests over
+
+/**
+ * This is a description of the component.
+ * It's multiple lines long.
+ */
+class ReactComponent extends Component {
+ render() {
+ return '';
+ }
+}
+
+ReactComponent.propTypes = {
+ children: React.PropTypes.node,
+ id: React.PropTypes.string.isRequired,
+};
+
+export default ReactComponent;
diff --git a/tests/development/__init__.py b/tests/development/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/development/flow_metadata_test.json b/tests/development/flow_metadata_test.json
new file mode 100644
index 0000000..bc93445
--- /dev/null
+++ b/tests/development/flow_metadata_test.json
@@ -0,0 +1,368 @@
+{
+ "description": "This is a test description of the component.\nIt's multiple lines long.",
+ "methods": [],
+ "props": {
+ "requiredString": {
+ "required": true,
+ "description": "A required string",
+ "flowType": {
+ "name": "string"
+ }
+ },
+ "optionalString": {
+ "required": false,
+ "description": "A string that isn't required.",
+ "flowType": {
+ "name": "string"
+ },
+ "defaultValue": {
+ "value": "''",
+ "computed": false
+ }
+ },
+ "optionalBoolean": {
+ "required": false,
+ "description": "A boolean test",
+ "flowType": {
+ "name": "boolean"
+ },
+ "defaultValue": {
+ "value": "false",
+ "computed": false
+ }
+ },
+ "optionalFunc": {
+ "required": false,
+ "description": "Dash callback to update props on the server",
+ "flowType": {
+ "name": "signature",
+ "type": "function",
+ "raw": "(props: { modal?: boolean, open?: boolean }) => void",
+ "signature": {
+ "arguments": [
+ {
+ "name": "props",
+ "type": {
+ "name": "signature",
+ "type": "object",
+ "raw": "{ modal?: boolean, open?: boolean }",
+ "signature": {
+ "properties": [
+ {
+ "key": "modal",
+ "value": {
+ "name": "boolean",
+ "required": false
+ }
+ },
+ {
+ "key": "open",
+ "value": {
+ "name": "boolean",
+ "required": false
+ }
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "return": {
+ "name": "void"
+ }
+ }
+ },
+ "defaultValue": {
+ "value": "() => {}",
+ "computed": false
+ }
+ },
+ "optionalNode": {
+ "required": false,
+ "description": "A node test",
+ "flowType": {
+ "name": "Node"
+ },
+ "defaultValue": {
+ "value": "null",
+ "computed": false
+ }
+ },
+ "optionalArray": {
+ "required": false,
+ "description": "An array test with a particularly \nlong description that covers several lines. It includes the newline character \nand should span 3 lines in total.",
+ "flowType": {
+ "name": "Array",
+ "elements": [
+ {
+ "name": "signature",
+ "type": "object",
+ "raw": "{\n checked?: boolean,\n children?: Node,\n customData: any,\n disabled?: boolean,\n label?: string,\n primaryText: string,\n secondaryText?: string,\n style?: Object,\n value: any,\n}",
+ "signature": {
+ "properties": [
+ {
+ "key": "checked",
+ "value": {
+ "name": "boolean",
+ "required": false
+ }
+ },
+ {
+ "key": "children",
+ "value": {
+ "name": "Node",
+ "required": false
+ }
+ },
+ {
+ "key": "customData",
+ "value": {
+ "name": "any",
+ "required": true
+ }
+ },
+ {
+ "key": "disabled",
+ "value": {
+ "name": "boolean",
+ "required": false
+ }
+ },
+ {
+ "key": "label",
+ "value": {
+ "name": "string",
+ "required": false
+ }
+ },
+ {
+ "key": "primaryText",
+ "value": {
+ "name": "string",
+ "required": true
+ }
+ },
+ {
+ "key": "secondaryText",
+ "value": {
+ "name": "string",
+ "required": false
+ }
+ },
+ {
+ "key": "style",
+ "value": {
+ "name": "Object",
+ "required": false
+ }
+ },
+ {
+ "key": "value",
+ "value": {
+ "name": "any",
+ "required": true
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "raw": "Array"
+ },
+ "defaultValue": {
+ "value": "[]",
+ "computed": false
+ }
+ },
+ "requiredUnion": {
+ "required": true,
+ "description": "",
+ "flowType": {
+ "name": "union",
+ "raw": "string | number",
+ "elements": [
+ {
+ "name": "string"
+ },
+ {
+ "name": "number"
+ }
+ ]
+ }
+ },
+ "optionalSignature(shape)": {
+ "flowType": {
+ "name": "signature",
+ "type": "object",
+ "raw": "{\n checked?: boolean,\n children?: Node,\n customData: any,\n disabled?: boolean,\n label?: string,\n primaryText: string,\n secondaryText?: string,\n style?: Object,\n value: any,\n}",
+ "signature": {
+ "properties": [
+ {
+ "key": "checked",
+ "value": {
+ "name": "boolean",
+ "required": false
+ }
+ },
+ {
+ "key": "children",
+ "value": {
+ "name": "Node",
+ "required": false
+ }
+ },
+ {
+ "key": "customData",
+ "value": {
+ "name": "any",
+ "required": true,
+ "description": "A test description"
+ }
+ },
+ {
+ "key": "disabled",
+ "value": {
+ "name": "boolean",
+ "required": false
+ }
+ },
+ {
+ "key": "label",
+ "value": {
+ "name": "string",
+ "required": false
+ }
+ },
+ {
+ "key": "primaryText",
+ "value": {
+ "name": "string",
+ "required": true,
+ "description": "Another test description"
+ }
+ },
+ {
+ "key": "secondaryText",
+ "value": {
+ "name": "string",
+ "required": false
+ }
+ },
+ {
+ "key": "style",
+ "value": {
+ "name": "Object",
+ "required": false
+ }
+ },
+ {
+ "key": "value",
+ "value": {
+ "name": "any",
+ "required": true
+ }
+ }
+ ]
+ }
+ },
+ "required": false,
+ "description": "This is a test of an object's shape"
+ },
+ "requiredNested": {
+ "flowType": {
+ "name": "signature",
+ "type": "object",
+ "raw": "{\n customData: SD_MENU_ITEM,\n value: any,\n}",
+ "signature": {
+ "properties": [
+ {
+ "key": "customData",
+ "value": {
+ "name": "signature",
+ "type": "object",
+ "raw": "{\n checked?: boolean,\n children?: Node,\n customData: any,\n disabled?: boolean,\n label?: string,\n primaryText: string,\n secondaryText?: string,\n style?: Object,\n value: any,\n}",
+ "signature": {
+ "properties": [
+ {
+ "key": "checked",
+ "value": {
+ "name": "boolean",
+ "required": false
+ }
+ },
+ {
+ "key": "children",
+ "value": {
+ "name": "Node",
+ "required": false
+ }
+ },
+ {
+ "key": "customData",
+ "value": {
+ "name": "any",
+ "required": true
+ }
+ },
+ {
+ "key": "disabled",
+ "value": {
+ "name": "boolean",
+ "required": false
+ }
+ },
+ {
+ "key": "label",
+ "value": {
+ "name": "string",
+ "required": false
+ }
+ },
+ {
+ "key": "primaryText",
+ "value": {
+ "name": "string",
+ "required": true
+ }
+ },
+ {
+ "key": "secondaryText",
+ "value": {
+ "name": "string",
+ "required": false
+ }
+ },
+ {
+ "key": "style",
+ "value": {
+ "name": "Object",
+ "required": false
+ }
+ },
+ {
+ "key": "value",
+ "value": {
+ "name": "any",
+ "required": true
+ }
+ }
+ ]
+ },
+ "required": true
+ }
+ },
+ {
+ "key": "value",
+ "value": {
+ "name": "any",
+ "required": true
+ }
+ }
+ ]
+ }
+ },
+ "required": true,
+ "description": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/development/metadata_required_test.json b/tests/development/metadata_required_test.json
new file mode 100644
index 0000000..9b2caa6
--- /dev/null
+++ b/tests/development/metadata_required_test.json
@@ -0,0 +1,20 @@
+{
+ "description": "This is a description of the component.\nIt's multiple lines long.",
+ "methods": [],
+ "props": {
+ "children": {
+ "type": {
+ "name": "node"
+ },
+ "required": false,
+ "description": ""
+ },
+ "id": {
+ "type": {
+ "name": "string"
+ },
+ "required": true,
+ "description": ""
+ }
+ }
+}
diff --git a/tests/development/metadata_test.json b/tests/development/metadata_test.json
new file mode 100644
index 0000000..1da85ba
--- /dev/null
+++ b/tests/development/metadata_test.json
@@ -0,0 +1,260 @@
+{
+ "description": "This is a description of the component.\nIt's multiple lines long.",
+ "methods": [],
+ "props": {
+ "optionalArray": {
+ "type": {
+ "name": "array"
+ },
+ "required": false,
+ "description": "Description of optionalArray"
+ },
+ "optionalBool": {
+ "type": {
+ "name": "bool"
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalFunc": {
+ "type": {
+ "name": "func"
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalNumber": {
+ "type": {
+ "name": "number"
+ },
+ "required": false,
+ "description": "",
+ "defaultValue": {
+ "value": "42",
+ "computed": false
+ }
+ },
+ "optionalObject": {
+ "type": {
+ "name": "object"
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalString": {
+ "type": {
+ "name": "string"
+ },
+ "required": false,
+ "description": "",
+ "defaultValue": {
+ "value": "'hello world'",
+ "computed": false
+ }
+ },
+ "optionalSymbol": {
+ "type": {
+ "name": "symbol"
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalNode": {
+ "type": {
+ "name": "node"
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalElement": {
+ "type": {
+ "name": "element"
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalMessage": {
+ "type": {
+ "name": "instanceOf",
+ "value": "Message"
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalEnum": {
+ "type": {
+ "name": "enum",
+ "value": [
+ {
+ "value": "'News'",
+ "computed": false
+ },
+ {
+ "value": "'Photos'",
+ "computed": false
+ }
+ ]
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalUnion": {
+ "type": {
+ "name": "union",
+ "value": [
+ {
+ "name": "string"
+ },
+ {
+ "name": "number"
+ },
+ {
+ "name": "instanceOf",
+ "value": "Message"
+ }
+ ]
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalArrayOf": {
+ "type": {
+ "name": "arrayOf",
+ "value": {
+ "name": "number"
+ }
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalObjectOf": {
+ "type": {
+ "name": "objectOf",
+ "value": {
+ "name": "number"
+ }
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalObjectWithShapeAndNestedDescription": {
+ "type": {
+ "name": "shape",
+ "value": {
+ "color": {
+ "name": "string",
+ "required": false
+ },
+ "fontSize": {
+ "name": "number",
+ "required": false
+ },
+ "figure": {
+ "name": "shape",
+ "value": {
+ "data": {
+ "name": "arrayOf",
+ "value": {
+ "name": "object"
+ },
+ "description": "data is a collection of traces",
+ "required": false
+ },
+ "layout": {
+ "name": "object",
+ "description": "layout describes the rest of the figure",
+ "required": false
+ }
+ },
+ "description": "Figure is a plotly graph object",
+ "required": false
+ }
+ }
+ },
+ "required": false,
+ "description": ""
+ },
+ "optionalAny": {
+ "type": {
+ "name": "any"
+ },
+ "required": false,
+ "description": ""
+ },
+ "customProp": {
+ "type": {
+ "name": "custom",
+ "raw": "function(props, propName, componentName) {\n if (!/matchme/.test(props[propName])) {\n return new Error(\n 'Invalid prop `' + propName + '` supplied to' +\n ' `' + componentName + '`. Validation failed.'\n );\n }\n}"
+ },
+ "required": false,
+ "description": ""
+ },
+ "customArrayProp": {
+ "type": {
+ "name": "arrayOf",
+ "value": {
+ "name": "custom",
+ "raw": "function(propValue, key, componentName, location, propFullName) {\n if (!/matchme/.test(propValue[key])) {\n return new Error(\n 'Invalid prop `' + propFullName + '` supplied to' +\n ' `' + componentName + '`. Validation failed.'\n );\n }\n}"
+ }
+ },
+ "required": false,
+ "description": ""
+ },
+ "children": {
+ "type": {
+ "name": "node"
+ },
+ "required": false,
+ "description": ""
+ },
+ "data-*": {
+ "type": {
+ "name": "string"
+ },
+ "required": false,
+ "description": ""
+ },
+ "aria-*": {
+ "type": {
+ "name": "string"
+ },
+ "required": false,
+ "description": ""
+ },
+ "in": {
+ "type": {
+ "name": "string"
+ },
+ "required": false,
+ "description": ""
+ },
+ "id": {
+ "type": {
+ "name": "string"
+ },
+ "required": false,
+ "description": ""
+ },
+ "dashEvents": {
+ "type": {
+ "name": "enum",
+ "value": [
+ {
+ "value": "'restyle'",
+ "computed": false
+ },
+ {
+ "value": "'relayout'",
+ "computed": false
+ },
+ {
+ "value": "'click'",
+ "computed": false
+ }
+ ]
+ },
+ "required": false,
+ "description": ""
+ }
+ }
+}
diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py
new file mode 100644
index 0000000..1074ff0
--- /dev/null
+++ b/tests/development/metadata_test.py
@@ -0,0 +1,83 @@
+# AUTO GENERATED FILE - DO NOT EDIT
+
+from dash.development.base_component import Component, _explicitize_args
+
+
+class Table(Component):
+ """A Table component.
+This is a description of the component.
+It's multiple lines long.
+
+Keyword arguments:
+- children (a list of or a singular dash component, string or number; optional)
+- optionalArray (list; optional): Description of optionalArray
+- optionalBool (boolean; optional)
+- optionalNumber (number; optional)
+- optionalObject (dict; optional)
+- optionalString (string; optional)
+- optionalNode (a list of or a singular dash component, string or number; optional)
+- optionalElement (dash component; optional)
+- optionalEnum (a value equal to: 'News', 'Photos'; optional)
+- optionalUnion (string | number; optional)
+- optionalArrayOf (list; optional)
+- optionalObjectOf (dict with strings as keys and values of type number; optional)
+- optionalObjectWithShapeAndNestedDescription (optional): . optionalObjectWithShapeAndNestedDescription has the following type: dict containing keys 'color', 'fontSize', 'figure'.
+Those keys have the following types:
+ - color (string; optional)
+ - fontSize (number; optional)
+ - figure (optional): Figure is a plotly graph object. figure has the following type: dict containing keys 'data', 'layout'.
+Those keys have the following types:
+ - data (list; optional): data is a collection of traces
+ - layout (dict; optional): layout describes the rest of the figure
+- optionalAny (boolean | number | string | dict | list; optional)
+- customProp (optional)
+- customArrayProp (list; optional)
+- data-* (string; optional)
+- aria-* (string; optional)
+- in (string; optional)
+- id (string; optional)
+
+Available events: 'restyle', 'relayout', 'click'"""
+ @_explicitize_args
+ def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBool=Component.UNDEFINED, optionalFunc=Component.UNDEFINED, optionalNumber=Component.UNDEFINED, optionalObject=Component.UNDEFINED, optionalString=Component.UNDEFINED, optionalSymbol=Component.UNDEFINED, optionalNode=Component.UNDEFINED, optionalElement=Component.UNDEFINED, optionalMessage=Component.UNDEFINED, optionalEnum=Component.UNDEFINED, optionalUnion=Component.UNDEFINED, optionalArrayOf=Component.UNDEFINED, optionalObjectOf=Component.UNDEFINED, optionalObjectWithShapeAndNestedDescription=Component.UNDEFINED, optionalAny=Component.UNDEFINED, customProp=Component.UNDEFINED, customArrayProp=Component.UNDEFINED, id=Component.UNDEFINED, **kwargs):
+ self._prop_names = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'in', 'id']
+ self._type = 'Table'
+ self._namespace = 'TableComponents'
+ self._valid_wildcard_attributes = ['data-', 'aria-']
+ self.available_events = ['restyle', 'relayout', 'click']
+ self.available_properties = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'in', 'id']
+ self.available_wildcard_properties = ['data-', 'aria-']
+
+ _explicit_args = kwargs.pop('_explicit_args')
+ _locals = locals()
+ _locals.update(kwargs) # For wildcard attrs
+ args = {k: _locals[k] for k in _explicit_args if k != 'children'}
+
+ for k in []:
+ if k not in args:
+ raise TypeError(
+ 'Required argument `' + k + '` was not specified.')
+ super(Table, self).__init__(children=children, **args)
+
+ def __repr__(self):
+ if(any(getattr(self, c, None) is not None
+ for c in self._prop_names
+ if c is not self._prop_names[0])
+ or any(getattr(self, c, None) is not None
+ for c in self.__dict__.keys()
+ if any(c.startswith(wc_attr)
+ for wc_attr in self._valid_wildcard_attributes))):
+ props_string = ', '.join([c+'='+repr(getattr(self, c, None))
+ for c in self._prop_names
+ if getattr(self, c, None) is not None])
+ wilds_string = ', '.join([c+'='+repr(getattr(self, c, None))
+ for c in self.__dict__.keys()
+ if any([c.startswith(wc_attr)
+ for wc_attr in
+ self._valid_wildcard_attributes])])
+ return ('Table(' + props_string +
+ (', ' + wilds_string if wilds_string != '' else '') + ')')
+ else:
+ return (
+ 'Table(' +
+ repr(getattr(self, self._prop_names[0], None)) + ')')
diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py
new file mode 100644
index 0000000..bb32cc7
--- /dev/null
+++ b/tests/development/test_base_component.py
@@ -0,0 +1,1042 @@
+from collections import OrderedDict
+import collections
+import inspect
+import json
+import os
+import shutil
+import unittest
+import plotly
+
+from dash.development.base_component import (
+ generate_class,
+ generate_class_string,
+ generate_class_file,
+ Component,
+ _explicitize_args,
+ js_to_py_type,
+ create_docstring,
+ parse_events
+)
+
+Component._prop_names = ('id', 'a', 'children', 'style', )
+Component._type = 'TestComponent'
+Component._namespace = 'test_namespace'
+Component._valid_wildcard_attributes = ['data-', 'aria-']
+
+
+def nested_tree():
+ """This tree has a few unique properties:
+ - children is mixed strings and components (as in c2)
+ - children is just components (as in c)
+ - children is just strings (as in c1)
+ - children is just a single component (as in c3, c4)
+ - children contains numbers (as in c2)
+ - children contains "None" items (as in c2)
+ """
+ c1 = Component(
+ id='0.1.x.x.0',
+ children='string'
+ )
+ c2 = Component(
+ id='0.1.x.x',
+ children=[10, None, 'wrap string', c1, 'another string', 4.51]
+ )
+ c3 = Component(
+ id='0.1.x',
+ # children is just a component
+ children=c2
+ )
+ c4 = Component(
+ id='0.1',
+ children=c3
+ )
+ c5 = Component(id='0.0')
+ c = Component(id='0', children=[c5, c4])
+ return c, c1, c2, c3, c4, c5
+
+
+class TestComponent(unittest.TestCase):
+ def test_init(self):
+ Component(a=3)
+
+ def test_get_item_with_children(self):
+ c1 = Component(id='1')
+ c2 = Component(children=[c1])
+ self.assertEqual(c2['1'], c1)
+
+ def test_get_item_with_children_as_component_instead_of_list(self):
+ c1 = Component(id='1')
+ c2 = Component(id='2', children=c1)
+ self.assertEqual(c2['1'], c1)
+
+ def test_get_item_with_nested_children_one_branch(self):
+ c1 = Component(id='1')
+ c2 = Component(id='2', children=[c1])
+ c3 = Component(children=[c2])
+ self.assertEqual(c2['1'], c1)
+ self.assertEqual(c3['2'], c2)
+ self.assertEqual(c3['1'], c1)
+
+ def test_get_item_with_nested_children_two_branches(self):
+ c1 = Component(id='1')
+ c2 = Component(id='2', children=[c1])
+ c3 = Component(id='3')
+ c4 = Component(id='4', children=[c3])
+ c5 = Component(children=[c2, c4])
+ self.assertEqual(c2['1'], c1)
+ self.assertEqual(c4['3'], c3)
+ self.assertEqual(c5['2'], c2)
+ self.assertEqual(c5['4'], c4)
+ self.assertEqual(c5['1'], c1)
+ self.assertEqual(c5['3'], c3)
+
+ def test_get_item_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501
+ c, c1, c2, c3, c4, c5 = nested_tree()
+ self.assertEqual(
+ list(c.keys()),
+ [
+ '0.0',
+ '0.1',
+ '0.1.x',
+ '0.1.x.x',
+ '0.1.x.x.0'
+ ]
+ )
+
+ # Try to get each item
+ for comp in [c1, c2, c3, c4, c5]:
+ self.assertEqual(c[comp.id], comp)
+
+ # Get an item that doesn't exist
+ with self.assertRaises(KeyError):
+ c['x']
+
+ def test_len_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501
+ c = nested_tree()[0]
+ self.assertEqual(
+ len(c),
+ 5 + # 5 components
+ 5 + # c2 has 2 strings, 2 numbers, and a None
+ 1 # c1 has 1 string
+ )
+
+ def test_set_item_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501
+ keys = [
+ '0.0',
+ '0.1',
+ '0.1.x',
+ '0.1.x.x',
+ '0.1.x.x.0'
+ ]
+ c = nested_tree()[0]
+
+ # Test setting items starting from the innermost item
+ for key in reversed(keys):
+ new_id = 'new {}'.format(key)
+ new_component = Component(
+ id=new_id,
+ children='new string'
+ )
+ c[key] = new_component
+ self.assertEqual(c[new_id], new_component)
+
+ def test_del_item_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501
+ c = nested_tree()[0]
+ for key in reversed(list(c.keys())):
+ c[key]
+ del c[key]
+ with self.assertRaises(KeyError):
+ c[key]
+
+ def test_traverse_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501
+ c, c1, c2, c3, c4, c5 = nested_tree()
+ elements = [i for i in c.traverse()]
+ self.assertEqual(
+ elements,
+ c.children + [c3] + [c2] + c2.children
+ )
+
+ def test_iter_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501
+ c = nested_tree()[0]
+ keys = list(c.keys())
+ # get a list of ids that __iter__ provides
+ iter_keys = [i for i in c]
+ self.assertEqual(keys, iter_keys)
+
+ def test_to_plotly_json_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501
+ c = nested_tree()[0]
+ Component._namespace
+ Component._type
+
+ self.assertEqual(json.loads(json.dumps(
+ c.to_plotly_json(),
+ cls=plotly.utils.PlotlyJSONEncoder
+ )), {
+ 'type': 'TestComponent',
+ 'namespace': 'test_namespace',
+ 'props': {
+ 'children': [
+ {
+ 'type': 'TestComponent',
+ 'namespace': 'test_namespace',
+ 'props': {
+ 'id': '0.0'
+ }
+ },
+ {
+ 'type': 'TestComponent',
+ 'namespace': 'test_namespace',
+ 'props': {
+ 'children': {
+ 'type': 'TestComponent',
+ 'namespace': 'test_namespace',
+ 'props': {
+ 'children': {
+ 'type': 'TestComponent',
+ 'namespace': 'test_namespace',
+ 'props': {
+ 'children': [
+ 10,
+ None,
+ 'wrap string',
+ {
+ 'type': 'TestComponent',
+ 'namespace': 'test_namespace', # noqa: E501
+ 'props': {
+ 'children': 'string',
+ 'id': '0.1.x.x.0'
+ }
+ },
+ 'another string',
+ 4.51
+ ],
+ 'id': '0.1.x.x'
+ }
+ },
+ 'id': '0.1.x'
+ }
+ },
+ 'id': '0.1'
+ }
+ }
+ ],
+ 'id': '0'
+ }
+ })
+
+ def test_get_item_raises_key_if_id_doesnt_exist(self):
+ c = Component()
+ with self.assertRaises(KeyError):
+ c['1']
+
+ c1 = Component(id='1')
+ with self.assertRaises(KeyError):
+ c1['1']
+
+ c2 = Component(id='2', children=[c1])
+ with self.assertRaises(KeyError):
+ c2['0']
+
+ c3 = Component(children='string with no id')
+ with self.assertRaises(KeyError):
+ c3['0']
+
+ def test_equality(self):
+ # TODO - Why is this the case? How is == being performed?
+ # __eq__ only needs __getitem__, __iter__, and __len__
+ self.assertTrue(Component() == Component())
+ self.assertTrue(Component() is not Component())
+
+ c1 = Component(id='1')
+ c2 = Component(id='2', children=[Component()])
+ self.assertTrue(c1 == c2)
+ self.assertTrue(c1 is not c2)
+
+ def test_set_item(self):
+ c1a = Component(id='1', children='Hello world')
+ c2 = Component(id='2', children=c1a)
+ self.assertEqual(c2['1'], c1a)
+ c1b = Component(id='1', children='Brave new world')
+ c2['1'] = c1b
+ self.assertEqual(c2['1'], c1b)
+
+ def test_set_item_with_children_as_list(self):
+ c1 = Component(id='1')
+ c2 = Component(id='2', children=[c1])
+ self.assertEqual(c2['1'], c1)
+ c3 = Component(id='3')
+ c2['1'] = c3
+ self.assertEqual(c2['3'], c3)
+
+ def test_set_item_with_nested_children(self):
+ c1 = Component(id='1')
+ c2 = Component(id='2', children=[c1])
+ c3 = Component(id='3')
+ c4 = Component(id='4', children=[c3])
+ c5 = Component(id='5', children=[c2, c4])
+
+ c3b = Component(id='3')
+ self.assertEqual(c5['3'], c3)
+ self.assertTrue(c5['3'] is not '3')
+ self.assertTrue(c5['3'] is not c3b)
+
+ c5['3'] = c3b
+ self.assertTrue(c5['3'] is c3b)
+ self.assertTrue(c5['3'] is not c3)
+
+ c2b = Component(id='2')
+ c5['2'] = c2b
+ self.assertTrue(c5['4'] is c4)
+ self.assertTrue(c5['2'] is not c2)
+ self.assertTrue(c5['2'] is c2b)
+ with self.assertRaises(KeyError):
+ c5['1']
+
+ def test_set_item_raises_key_error(self):
+ c1 = Component(id='1')
+ c2 = Component(id='2', children=[c1])
+ with self.assertRaises(KeyError):
+ c2['3'] = Component(id='3')
+
+ def test_del_item_from_list(self):
+ c1 = Component(id='1')
+ c2 = Component(id='2')
+ c3 = Component(id='3', children=[c1, c2])
+ self.assertEqual(c3['1'], c1)
+ self.assertEqual(c3['2'], c2)
+ del c3['2']
+ with self.assertRaises(KeyError):
+ c3['2']
+ self.assertEqual(c3.children, [c1])
+
+ del c3['1']
+ with self.assertRaises(KeyError):
+ c3['1']
+ self.assertEqual(c3.children, [])
+
+ def test_del_item_from_class(self):
+ c1 = Component(id='1')
+ c2 = Component(id='2', children=c1)
+ self.assertEqual(c2['1'], c1)
+ del c2['1']
+ with self.assertRaises(KeyError):
+ c2['1']
+
+ self.assertEqual(c2.children, None)
+
+ def test_to_plotly_json_without_children(self):
+ c = Component(id='a')
+ c._prop_names = ('id',)
+ c._type = 'MyComponent'
+ c._namespace = 'basic'
+ self.assertEqual(
+ c.to_plotly_json(),
+ {'namespace': 'basic', 'props': {'id': 'a'}, 'type': 'MyComponent'}
+ )
+
+ def test_to_plotly_json_with_null_arguments(self):
+ c = Component(id='a')
+ c._prop_names = ('id', 'style',)
+ c._type = 'MyComponent'
+ c._namespace = 'basic'
+ self.assertEqual(
+ c.to_plotly_json(),
+ {'namespace': 'basic', 'props': {'id': 'a'}, 'type': 'MyComponent'}
+ )
+
+ c = Component(id='a', style=None)
+ c._prop_names = ('id', 'style',)
+ c._type = 'MyComponent'
+ c._namespace = 'basic'
+ self.assertEqual(
+ c.to_plotly_json(),
+ {
+ 'namespace': 'basic', 'props': {'id': 'a', 'style': None},
+ 'type': 'MyComponent'
+ }
+ )
+
+ def test_to_plotly_json_with_children(self):
+ c = Component(id='a', children='Hello World')
+ c._prop_names = ('id', 'children',)
+ c._type = 'MyComponent'
+ c._namespace = 'basic'
+ self.assertEqual(
+ c.to_plotly_json(),
+ {
+ 'namespace': 'basic',
+ 'props': {
+ 'id': 'a',
+ # TODO - Rename 'children' to 'children'
+ 'children': 'Hello World'
+ },
+ 'type': 'MyComponent'
+ }
+ )
+
+ def test_to_plotly_json_with_nested_children(self):
+ c1 = Component(id='1', children='Hello World')
+ c1._prop_names = ('id', 'children',)
+ c1._type = 'MyComponent'
+ c1._namespace = 'basic'
+
+ c2 = Component(id='2', children=c1)
+ c2._prop_names = ('id', 'children',)
+ c2._type = 'MyComponent'
+ c2._namespace = 'basic'
+
+ c3 = Component(id='3', children='Hello World')
+ c3._prop_names = ('id', 'children',)
+ c3._type = 'MyComponent'
+ c3._namespace = 'basic'
+
+ c4 = Component(id='4', children=[c2, c3])
+ c4._prop_names = ('id', 'children',)
+ c4._type = 'MyComponent'
+ c4._namespace = 'basic'
+
+ def to_dict(id, children):
+ return {
+ 'namespace': 'basic',
+ 'props': {
+ 'id': id,
+ 'children': children
+ },
+ 'type': 'MyComponent'
+ }
+
+ """
+ self.assertEqual(
+ json.dumps(c4.to_plotly_json(),
+ cls=plotly.utils.PlotlyJSONEncoder),
+ json.dumps(to_dict('4', [
+ to_dict('2', to_dict('1', 'Hello World')),
+ to_dict('3', 'Hello World')
+ ]))
+ )
+ """
+
+ def test_to_plotly_json_with_wildcards(self):
+ c = Component(id='a', **{'aria-expanded': 'true',
+ 'data-toggle': 'toggled',
+ 'data-none': None})
+ c._prop_names = ('id',)
+ c._type = 'MyComponent'
+ c._namespace = 'basic'
+ self.assertEqual(
+ c.to_plotly_json(),
+ {'namespace': 'basic',
+ 'props': {
+ 'aria-expanded': 'true',
+ 'data-toggle': 'toggled',
+ 'data-none': None,
+ 'id': 'a',
+ },
+ 'type': 'MyComponent'}
+ )
+
+ def test_len(self):
+ self.assertEqual(len(Component()), 0)
+ self.assertEqual(len(Component(children='Hello World')), 1)
+ self.assertEqual(len(Component(children=Component())), 1)
+ self.assertEqual(len(Component(children=[Component(), Component()])),
+ 2)
+ self.assertEqual(len(Component(children=[
+ Component(children=Component()),
+ Component()
+ ])), 3)
+
+ def test_iter(self):
+ # keys, __contains__, items, values, and more are all mixin methods
+ # that we get for free by inheriting from the MutableMapping
+ # and behave as according to our implementation of __iter__
+
+ c = Component(
+ id='1',
+ children=[
+ Component(id='2', children=[
+ Component(id='3', children=Component(id='4'))
+ ]),
+ Component(id='5', children=[
+ Component(id='6', children='Hello World')
+ ]),
+ Component(),
+ Component(children='Hello World'),
+ Component(children=Component(id='7')),
+ Component(children=[Component(id='8')]),
+ ]
+ )
+ # test keys()
+ keys = [k for k in list(c.keys())]
+ self.assertEqual(keys, ['2', '3', '4', '5', '6', '7', '8'])
+ self.assertEqual([i for i in c], keys)
+
+ # test values()
+ components = [i for i in list(c.values())]
+ self.assertEqual(components, [c[k] for k in keys])
+
+ # test __iter__()
+ for k in keys:
+ # test __contains__()
+ self.assertTrue(k in c)
+
+ # test __items__
+ items = [i for i in list(c.items())]
+ self.assertEqual(list(zip(keys, components)), items)
+
+ def test_pop(self):
+ c2 = Component(id='2')
+ c = Component(id='1', children=c2)
+ c2_popped = c.pop('2')
+ self.assertTrue('2' not in c)
+ self.assertTrue(c2_popped is c2)
+
+
+class TestGenerateClassFile(unittest.TestCase):
+ def setUp(self):
+ json_path = os.path.join('tests', 'development', 'metadata_test.json')
+ with open(json_path) as data_file:
+ json_string = data_file.read()
+ data = json\
+ .JSONDecoder(object_pairs_hook=collections.OrderedDict)\
+ .decode(json_string)
+ self.data = data
+
+ # Create a folder for the new component file
+ os.makedirs('TableComponents')
+
+ # Import string not included in generated class string
+ import_string =\
+ "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \
+ "from dash.development.base_component import" + \
+ " Component, _explicitize_args\n\n\n"
+
+ # Class string generated from generate_class_string
+ self.component_class_string = import_string + generate_class_string(
+ typename='Table',
+ props=data['props'],
+ description=data['description'],
+ namespace='TableComponents'
+ )
+
+ # Class string written to file
+ generate_class_file(
+ typename='Table',
+ props=data['props'],
+ description=data['description'],
+ namespace='TableComponents'
+ )
+ written_file_path = os.path.join(
+ 'TableComponents', "Table.py"
+ )
+ with open(written_file_path, 'r') as f:
+ self.written_class_string = f.read()
+
+ # The expected result for both class string and class file generation
+ expected_string_path = os.path.join(
+ 'tests', 'development', 'metadata_test.py'
+ )
+ with open(expected_string_path, 'r') as f:
+ self.expected_class_string = f.read()
+
+ def tearDown(self):
+ shutil.rmtree('TableComponents')
+
+ def test_class_string(self):
+ self.assertEqual(
+ self.expected_class_string,
+ self.component_class_string
+ )
+
+ def test_class_file(self):
+ self.assertEqual(
+ self.expected_class_string,
+ self.written_class_string
+ )
+
+
+class TestGenerateClass(unittest.TestCase):
+ def setUp(self):
+ path = os.path.join('tests', 'development', 'metadata_test.json')
+ with open(path) as data_file:
+ json_string = data_file.read()
+ data = json\
+ .JSONDecoder(object_pairs_hook=collections.OrderedDict)\
+ .decode(json_string)
+ self.data = data
+
+ self.ComponentClass = generate_class(
+ typename='Table',
+ props=data['props'],
+ description=data['description'],
+ namespace='TableComponents'
+ )
+
+ path = os.path.join(
+ 'tests', 'development', 'metadata_required_test.json'
+ )
+ with open(path) as data_file:
+ json_string = data_file.read()
+ required_data = json\
+ .JSONDecoder(object_pairs_hook=collections.OrderedDict)\
+ .decode(json_string)
+ self.required_data = required_data
+
+ self.ComponentClassRequired = generate_class(
+ typename='TableRequired',
+ props=required_data['props'],
+ description=required_data['description'],
+ namespace='TableComponents'
+ )
+
+ def test_to_plotly_json(self):
+ c = self.ComponentClass()
+ self.assertEqual(c.to_plotly_json(), {
+ 'namespace': 'TableComponents',
+ 'type': 'Table',
+ 'props': {
+ 'children': None
+ }
+ })
+
+ c = self.ComponentClass(id='my-id')
+ self.assertEqual(c.to_plotly_json(), {
+ 'namespace': 'TableComponents',
+ 'type': 'Table',
+ 'props': {
+ 'children': None,
+ 'id': 'my-id'
+ }
+ })
+
+ c = self.ComponentClass(id='my-id', optionalArray=None)
+ self.assertEqual(c.to_plotly_json(), {
+ 'namespace': 'TableComponents',
+ 'type': 'Table',
+ 'props': {
+ 'children': None,
+ 'id': 'my-id',
+ 'optionalArray': None
+ }
+ })
+
+ def test_arguments_become_attributes(self):
+ kwargs = {
+ 'id': 'my-id',
+ 'children': 'text children',
+ 'optionalArray': [[1, 2, 3]]
+ }
+ component_instance = self.ComponentClass(**kwargs)
+ for k, v in list(kwargs.items()):
+ self.assertEqual(getattr(component_instance, k), v)
+
+ def test_repr_single_default_argument(self):
+ c1 = self.ComponentClass('text children')
+ c2 = self.ComponentClass(children='text children')
+ self.assertEqual(
+ repr(c1),
+ "Table('text children')"
+ )
+ self.assertEqual(
+ repr(c2),
+ "Table('text children')"
+ )
+
+ def test_repr_single_non_default_argument(self):
+ c = self.ComponentClass(id='my-id')
+ self.assertEqual(
+ repr(c),
+ "Table(id='my-id')"
+ )
+
+ def test_repr_multiple_arguments(self):
+ # Note how the order in which keyword arguments are supplied is
+ # not always equal to the order in the repr of the component
+ c = self.ComponentClass(id='my id', optionalArray=[1, 2, 3])
+ self.assertEqual(
+ repr(c),
+ "Table(optionalArray=[1, 2, 3], id='my id')"
+ )
+
+ def test_repr_nested_arguments(self):
+ c1 = self.ComponentClass(id='1')
+ c2 = self.ComponentClass(id='2', children=c1)
+ c3 = self.ComponentClass(children=c2)
+ self.assertEqual(
+ repr(c3),
+ "Table(Table(children=Table(id='1'), id='2'))"
+ )
+
+ def test_repr_with_wildcards(self):
+ c = self.ComponentClass(id='1', **{"data-one": "one",
+ "aria-two": "two"})
+ data_first = "Table(id='1', data-one='one', aria-two='two')"
+ aria_first = "Table(id='1', aria-two='two', data-one='one')"
+ repr_string = repr(c)
+ if not (repr_string == data_first or repr_string == aria_first):
+ raise Exception("%s\nDoes not equal\n%s\nor\n%s" %
+ (repr_string, data_first, aria_first))
+
+ def test_docstring(self):
+ assert_docstring(self.assertEqual, self.ComponentClass.__doc__)
+
+ def test_events(self):
+ self.assertEqual(
+ self.ComponentClass().available_events,
+ ['restyle', 'relayout', 'click']
+ )
+
+ # This one is kind of pointless now
+ def test_call_signature(self):
+ __init__func = self.ComponentClass.__init__
+ # TODO: Will break in Python 3
+ # http://stackoverflow.com/questions/2677185/
+ self.assertEqual(
+ inspect.getargspec(__init__func).args,
+ ['self',
+ 'children',
+ 'optionalArray',
+ 'optionalBool',
+ 'optionalFunc',
+ 'optionalNumber',
+ 'optionalObject',
+ 'optionalString',
+ 'optionalSymbol',
+ 'optionalNode',
+ 'optionalElement',
+ 'optionalMessage',
+ 'optionalEnum',
+ 'optionalUnion',
+ 'optionalArrayOf',
+ 'optionalObjectOf',
+ 'optionalObjectWithShapeAndNestedDescription',
+ 'optionalAny',
+ 'customProp',
+ 'customArrayProp',
+ 'id'] if hasattr(inspect, 'signature') else []
+
+
+ )
+ self.assertEqual(
+ inspect.getargspec(__init__func).varargs,
+ None if hasattr(inspect, 'signature') else 'args'
+ )
+ self.assertEqual(
+ inspect.getargspec(__init__func).keywords,
+ 'kwargs'
+ )
+ if hasattr(inspect, 'signature'):
+ self.assertEqual(
+ [str(x) for x in inspect.getargspec(__init__func).defaults],
+ ['None'] + ['undefined'] * 19
+ )
+
+ def test_required_props(self):
+ with self.assertRaises(Exception):
+ self.ComponentClassRequired()
+ self.ComponentClassRequired(id='test')
+ with self.assertRaises(Exception):
+ self.ComponentClassRequired(id='test', lahlah='test')
+ with self.assertRaises(Exception):
+ self.ComponentClassRequired(children='test')
+
+
+class TestMetaDataConversions(unittest.TestCase):
+ def setUp(self):
+ path = os.path.join('tests', 'development', 'metadata_test.json')
+ with open(path) as data_file:
+ json_string = data_file.read()
+ data = json\
+ .JSONDecoder(object_pairs_hook=collections.OrderedDict)\
+ .decode(json_string)
+ self.data = data
+
+ self.expected_arg_strings = OrderedDict([
+ ['children',
+ 'a list of or a singular dash component, string or number'],
+
+ ['optionalArray', 'list'],
+
+ ['optionalBool', 'boolean'],
+
+ ['optionalFunc', ''],
+
+ ['optionalNumber', 'number'],
+
+ ['optionalObject', 'dict'],
+
+ ['optionalString', 'string'],
+
+ ['optionalSymbol', ''],
+
+ ['optionalElement', 'dash component'],
+
+ ['optionalNode',
+ 'a list of or a singular dash component, string or number'],
+
+ ['optionalMessage', ''],
+
+ ['optionalEnum', 'a value equal to: \'News\', \'Photos\''],
+
+ ['optionalUnion', 'string | number'],
+
+ ['optionalArrayOf', 'list'],
+
+ ['optionalObjectOf',
+ 'dict with strings as keys and values of type number'],
+
+ ['optionalObjectWithShapeAndNestedDescription', '\n'.join([
+
+ "dict containing keys 'color', 'fontSize', 'figure'.",
+ "Those keys have the following types: ",
+ " - color (string; optional)",
+ " - fontSize (number; optional)",
+ " - figure (optional): Figure is a plotly graph object. figure has the following type: dict containing keys 'data', 'layout'.", # noqa: E501
+ "Those keys have the following types: ",
+ " - data (list; optional): data is a collection of traces",
+ " - layout (dict; optional): layout describes the rest of the figure" # noqa: E501
+
+ ])],
+
+ ['optionalAny', 'boolean | number | string | dict | list'],
+
+ ['customProp', ''],
+
+ ['customArrayProp', 'list'],
+
+ ['data-*', 'string'],
+
+ ['aria-*', 'string'],
+
+ ['in', 'string'],
+
+ ['id', 'string'],
+
+ ['dashEvents', "a value equal to: 'restyle', 'relayout', 'click'"]
+ ])
+
+ def test_docstring(self):
+ docstring = create_docstring(
+ 'Table',
+ self.data['props'],
+ parse_events(self.data['props']),
+ self.data['description'],
+ )
+ assert_docstring(self.assertEqual, docstring)
+
+ def test_docgen_to_python_args(self):
+
+ props = self.data['props']
+
+ for prop_name, prop in list(props.items()):
+ self.assertEqual(
+ js_to_py_type(prop['type']),
+ self.expected_arg_strings[prop_name]
+ )
+
+
+def assert_docstring(assertEqual, docstring):
+ for i, line in enumerate(docstring.split('\n')):
+ assertEqual(line, ([
+ "A Table component.",
+ "This is a description of the component.",
+ "It's multiple lines long.",
+ '',
+ "Keyword arguments:",
+ "- children (a list of or a singular dash component, string or number; optional)", # noqa: E501
+ "- optionalArray (list; optional): Description of optionalArray",
+ "- optionalBool (boolean; optional)",
+ "- optionalNumber (number; optional)",
+ "- optionalObject (dict; optional)",
+ "- optionalString (string; optional)",
+
+ "- optionalNode (a list of or a singular dash component, "
+ "string or number; optional)",
+
+ "- optionalElement (dash component; optional)",
+ "- optionalEnum (a value equal to: 'News', 'Photos'; optional)",
+ "- optionalUnion (string | number; optional)",
+ "- optionalArrayOf (list; optional)",
+
+ "- optionalObjectOf (dict with strings as keys and values "
+ "of type number; optional)",
+
+ "- optionalObjectWithShapeAndNestedDescription (optional): . "
+ "optionalObjectWithShapeAndNestedDescription has the "
+ "following type: dict containing keys "
+ "'color', 'fontSize', 'figure'.",
+
+ "Those keys have the following types: ",
+ " - color (string; optional)",
+ " - fontSize (number; optional)",
+
+ " - figure (optional): Figure is a plotly graph object. "
+ "figure has the following type: dict containing "
+ "keys 'data', 'layout'.",
+
+ "Those keys have the following types: ",
+ " - data (list; optional): data is a collection of traces",
+
+ " - layout (dict; optional): layout describes "
+ "the rest of the figure",
+
+ "- optionalAny (boolean | number | string | dict | "
+ "list; optional)",
+
+ "- customProp (optional)",
+ "- customArrayProp (list; optional)",
+ '- data-* (string; optional)',
+ '- aria-* (string; optional)',
+ '- in (string; optional)',
+ '- id (string; optional)',
+ '',
+ "Available events: 'restyle', 'relayout', 'click'",
+ ' '
+ ])[i]
+ )
+
+
+class TestFlowMetaDataConversions(unittest.TestCase):
+ def setUp(self):
+ path = os.path.join('tests', 'development', 'flow_metadata_test.json')
+ with open(path) as data_file:
+ json_string = data_file.read()
+ data = json\
+ .JSONDecoder(object_pairs_hook=collections.OrderedDict)\
+ .decode(json_string)
+ self.data = data
+
+ self.expected_arg_strings = OrderedDict([
+ ['children', 'a list of or a singular dash component, string or number'],
+
+ ['requiredString', 'string'],
+
+ ['optionalString', 'string'],
+
+ ['optionalBoolean', 'boolean'],
+
+ ['optionalFunc', ''],
+
+ ['optionalNode', 'a list of or a singular dash component, string or number'],
+
+ ['optionalArray', 'list'],
+
+ ['requiredUnion', 'string | number'],
+
+ ['optionalSignature(shape)', '\n'.join([
+
+ "dict containing keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', 'style', 'value'.",
+ "Those keys have the following types: ",
+ "- checked (boolean; optional)",
+ "- children (a list of or a singular dash component, string or number; optional)",
+ "- customData (bool | number | str | dict | list; required): A test description",
+ "- disabled (boolean; optional)",
+ "- label (string; optional)",
+ "- primaryText (string; required): Another test description",
+ "- secondaryText (string; optional)",
+ "- style (dict; optional)",
+ "- value (bool | number | str | dict | list; required)"
+
+ ])],
+
+ ['requiredNested', '\n'.join([
+
+ "dict containing keys 'customData', 'value'.",
+ "Those keys have the following types: ",
+ "- customData (required): . customData has the following type: dict containing keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', 'style', 'value'.",
+ " Those keys have the following types: ",
+ " - checked (boolean; optional)",
+ " - children (a list of or a singular dash component, string or number; optional)",
+ " - customData (bool | number | str | dict | list; required)",
+ " - disabled (boolean; optional)",
+ " - label (string; optional)",
+ " - primaryText (string; required)",
+ " - secondaryText (string; optional)",
+ " - style (dict; optional)",
+ " - value (bool | number | str | dict | list; required)",
+ "- value (bool | number | str | dict | list; required)",
+
+ ])],
+ ])
+
+ def test_docstring(self):
+ docstring = create_docstring(
+ 'Flow_component',
+ self.data['props'],
+ parse_events(self.data['props']),
+ self.data['description'],
+ )
+ assert_flow_docstring(self.assertEqual, docstring)
+
+ def test_docgen_to_python_args(self):
+
+ props = self.data['props']
+
+ for prop_name, prop in list(props.items()):
+ self.assertEqual(
+ js_to_py_type(prop['flowType'], is_flow_type=True),
+ self.expected_arg_strings[prop_name]
+ )
+
+
+def assert_flow_docstring(assertEqual, docstring):
+ for i, line in enumerate(docstring.split('\n')):
+ assertEqual(line, ([
+ "A Flow_component component.",
+ "This is a test description of the component.",
+ "It's multiple lines long.",
+ "",
+ "Keyword arguments:",
+ "- requiredString (string; required): A required string",
+ "- optionalString (string; optional): A string that isn't required.",
+ "- optionalBoolean (boolean; optional): A boolean test",
+
+ "- optionalNode (a list of or a singular dash component, string or number; optional): "
+ "A node test",
+
+ "- optionalArray (list; optional): An array test with a particularly ",
+ "long description that covers several lines. It includes the newline character ",
+ "and should span 3 lines in total.",
+
+ "- requiredUnion (string | number; required)",
+
+ "- optionalSignature(shape) (optional): This is a test of an object's shape. "
+ "optionalSignature(shape) has the following type: dict containing keys 'checked', "
+ "'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', "
+ "'style', 'value'.",
+
+ " Those keys have the following types: ",
+ " - checked (boolean; optional)",
+ " - children (a list of or a singular dash component, string or number; optional)",
+ " - customData (bool | number | str | dict | list; required): A test description",
+ " - disabled (boolean; optional)",
+ " - label (string; optional)",
+ " - primaryText (string; required): Another test description",
+ " - secondaryText (string; optional)",
+ " - style (dict; optional)",
+ " - value (bool | number | str | dict | list; required)",
+
+ "- requiredNested (required): . requiredNested has the following type: dict containing "
+ "keys 'customData', 'value'.",
+
+ " Those keys have the following types: ",
+
+ " - customData (required): . customData has the following type: dict containing "
+ "keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', "
+ "'secondaryText', 'style', 'value'.",
+
+ " Those keys have the following types: ",
+ " - checked (boolean; optional)",
+ " - children (a list of or a singular dash component, string or number; optional)",
+ " - customData (bool | number | str | dict | list; required)",
+ " - disabled (boolean; optional)",
+ " - label (string; optional)",
+ " - primaryText (string; required)",
+ " - secondaryText (string; optional)",
+ " - style (dict; optional)",
+ " - value (bool | number | str | dict | list; required)",
+ " - value (bool | number | str | dict | list; required)",
+ "",
+ "Available events: "
+ ])[i]
+ )
diff --git a/tests/development/test_component_loader.py b/tests/development/test_component_loader.py
new file mode 100644
index 0000000..b0f8266
--- /dev/null
+++ b/tests/development/test_component_loader.py
@@ -0,0 +1,221 @@
+import collections
+import json
+import os
+import shutil
+import unittest
+from dash.development.component_loader import load_components, generate_classes
+from dash.development.base_component import (
+ generate_class,
+ Component
+)
+
+METADATA_PATH = 'metadata.json'
+
+METADATA_STRING = '''{
+ "MyComponent.react.js": {
+ "props": {
+ "foo": {
+ "type": {
+ "name": "number"
+ },
+ "required": false,
+ "description": "Description of prop foo.",
+ "defaultValue": {
+ "value": "42",
+ "computed": false
+ }
+ },
+ "children": {
+ "type": {
+ "name": "object"
+ },
+ "description": "Children",
+ "required": false
+ },
+ "data-*": {
+ "type": {
+ "name": "string"
+ },
+ "description": "Wildcard data",
+ "required": false
+ },
+ "aria-*": {
+ "type": {
+ "name": "string"
+ },
+ "description": "Wildcard aria",
+ "required": false
+ },
+ "bar": {
+ "type": {
+ "name": "custom"
+ },
+ "required": false,
+ "description": "Description of prop bar.",
+ "defaultValue": {
+ "value": "21",
+ "computed": false
+ }
+ },
+ "baz": {
+ "type": {
+ "name": "union",
+ "value": [
+ {
+ "name": "number"
+ },
+ {
+ "name": "string"
+ }
+ ]
+ },
+ "required": false,
+ "description": ""
+ }
+ },
+ "description": "General component description.",
+ "methods": []
+ },
+ "A.react.js": {
+ "description": "",
+ "methods": [],
+ "props": {
+ "href": {
+ "type": {
+ "name": "string"
+ },
+ "required": false,
+ "description": "The URL of a linked resource."
+ },
+ "children": {
+ "type": {
+ "name": "object"
+ },
+ "description": "Children",
+ "required": false
+ }
+ }
+ }
+}'''
+METADATA = json\
+ .JSONDecoder(object_pairs_hook=collections.OrderedDict)\
+ .decode(METADATA_STRING)
+
+
+class TestLoadComponents(unittest.TestCase):
+ def setUp(self):
+ with open(METADATA_PATH, 'w') as f:
+ f.write(METADATA_STRING)
+
+ def tearDown(self):
+ os.remove(METADATA_PATH)
+
+ def test_loadcomponents(self):
+ MyComponent = generate_class(
+ 'MyComponent',
+ METADATA['MyComponent.react.js']['props'],
+ METADATA['MyComponent.react.js']['description'],
+ 'default_namespace'
+ )
+
+ A = generate_class(
+ 'A',
+ METADATA['A.react.js']['props'],
+ METADATA['A.react.js']['description'],
+ 'default_namespace'
+ )
+
+ c = load_components(METADATA_PATH)
+
+ MyComponentKwargs = {
+ 'foo': 'Hello World',
+ 'bar': 'Lah Lah',
+ 'baz': 'Lemons',
+ 'data-foo': 'Blah',
+ 'aria-bar': 'Seven',
+ 'children': 'Child'
+ }
+ AKwargs = {
+ 'children': 'Child',
+ 'href': 'Hello World'
+ }
+
+ self.assertTrue(
+ isinstance(MyComponent(**MyComponentKwargs), Component)
+ )
+
+ self.assertEqual(
+ repr(MyComponent(**MyComponentKwargs)),
+ repr(c[0](**MyComponentKwargs))
+ )
+
+ self.assertEqual(
+ repr(A(**AKwargs)),
+ repr(c[1](**AKwargs))
+ )
+
+
+class TestGenerateClasses(unittest.TestCase):
+ def setUp(self):
+ with open(METADATA_PATH, 'w') as f:
+ f.write(METADATA_STRING)
+ os.makedirs('default_namespace')
+
+ init_file_path = 'default_namespace/__init__.py'
+ with open(init_file_path, 'a'):
+ os.utime(init_file_path, None)
+
+ def tearDown(self):
+ os.remove(METADATA_PATH)
+ shutil.rmtree('default_namespace')
+
+ def test_loadcomponents(self):
+ MyComponent_runtime = generate_class(
+ 'MyComponent',
+ METADATA['MyComponent.react.js']['props'],
+ METADATA['MyComponent.react.js']['description'],
+ 'default_namespace'
+ )
+
+ A_runtime = generate_class(
+ 'A',
+ METADATA['A.react.js']['props'],
+ METADATA['A.react.js']['description'],
+ 'default_namespace'
+ )
+
+ generate_classes('default_namespace', METADATA_PATH)
+ from default_namespace.MyComponent import MyComponent \
+ as MyComponent_buildtime
+ from default_namespace.A import A as A_buildtime
+
+ MyComponentKwargs = {
+ 'foo': 'Hello World',
+ 'bar': 'Lah Lah',
+ 'baz': 'Lemons',
+ 'data-foo': 'Blah',
+ 'aria-bar': 'Seven',
+ 'baz': 'Lemons',
+ 'children': 'Child'
+ }
+ AKwargs = {
+ 'children': 'Child',
+ 'href': 'Hello World'
+ }
+
+ self.assertTrue(
+ isinstance(
+ MyComponent_buildtime(**MyComponentKwargs),
+ Component
+ )
+ )
+
+ self.assertEqual(
+ repr(MyComponent_buildtime(**MyComponentKwargs)),
+ repr(MyComponent_runtime(**MyComponentKwargs)),
+ )
+
+ self.assertEqual(
+ repr(A_runtime(**AKwargs)),
+ repr(A_buildtime(**AKwargs))
+ )
diff --git a/tests/django_project/__init__.py b/tests/django_project/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/django_project/dynamic_dash/__init__.py b/tests/django_project/dynamic_dash/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/django_project/dynamic_dash/apps.py b/tests/django_project/dynamic_dash/apps.py
new file mode 100644
index 0000000..5c71520
--- /dev/null
+++ b/tests/django_project/dynamic_dash/apps.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.apps import AppConfig
+
+# Initialise views for registering in dash.dash.BaseDashView._dashes
+from .views import *
+
+
+class App(AppConfig):
+ name = 'dynamic_dash'
diff --git a/tests/django_project/dynamic_dash/migrations/__init__.py b/tests/django_project/dynamic_dash/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/django_project/dynamic_dash/models.py b/tests/django_project/dynamic_dash/models.py
new file mode 100644
index 0000000..1dfab76
--- /dev/null
+++ b/tests/django_project/dynamic_dash/models.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models
+
+# Create your models here.
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/load_first.js b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/load_first.js
new file mode 100644
index 0000000..22c7799
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/load_first.js
@@ -0,0 +1,7 @@
+window.tested = ['load_first'];
+var ramdaTest = document.getElementById('ramda-test');
+if (ramdaTest) {
+ ramdaTest.innerHTML = R.join(' ', R.concat(['hello'], ['world']).map(function(x) {
+ return _.capitalize(x);
+ }));
+}
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/load_ignored.js b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/load_ignored.js
new file mode 100644
index 0000000..668e477
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/load_ignored.js
@@ -0,0 +1 @@
+window.tested = 'IGNORED'; // Break the chain.
\ No newline at end of file
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_css/nested.css b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_css/nested.css
new file mode 100644
index 0000000..24ba3d2
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_css/nested.css
@@ -0,0 +1,3 @@
+#content {
+ padding: 8px;
+}
\ No newline at end of file
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after.js b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after.js
new file mode 100644
index 0000000..6f520fd
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after.js
@@ -0,0 +1 @@
+window.tested.push('load_after');
\ No newline at end of file
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after1.js b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after1.js
new file mode 100644
index 0000000..1629d39
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after1.js
@@ -0,0 +1 @@
+window.tested.push('load_after1');
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after10.js b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after10.js
new file mode 100644
index 0000000..fcfdb59
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after10.js
@@ -0,0 +1 @@
+window.tested.push('load_after10');
\ No newline at end of file
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after11.js b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after11.js
new file mode 100644
index 0000000..bd11cd2
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after11.js
@@ -0,0 +1 @@
+window.tested.push('load_after11');
\ No newline at end of file
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after2.js b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after2.js
new file mode 100644
index 0000000..0b76a55
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after2.js
@@ -0,0 +1 @@
+window.tested.push('load_after2');
\ No newline at end of file
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after3.js b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after3.js
new file mode 100644
index 0000000..d913af9
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after3.js
@@ -0,0 +1 @@
+window.tested.push('load_after3');
\ No newline at end of file
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after4.js b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after4.js
new file mode 100644
index 0000000..2507e38
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_after4.js
@@ -0,0 +1 @@
+window.tested.push('load_after4');
\ No newline at end of file
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_last.js b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_last.js
new file mode 100644
index 0000000..285aa60
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/nested_js/load_last.js
@@ -0,0 +1 @@
+document.getElementById('tested').innerHTML = JSON.stringify(window.tested);
diff --git a/tests/django_project/dynamic_dash/static/dynamic_dash/assets/reset.css b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/reset.css
new file mode 100644
index 0000000..c7c7b58
--- /dev/null
+++ b/tests/django_project/dynamic_dash/static/dynamic_dash/assets/reset.css
@@ -0,0 +1,2 @@
+body {margin: 0;}
+button {height: 18px}
diff --git a/tests/django_project/dynamic_dash/views.py b/tests/django_project/dynamic_dash/views.py
new file mode 100644
index 0000000..ca10895
--- /dev/null
+++ b/tests/django_project/dynamic_dash/views.py
@@ -0,0 +1,373 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import datetime
+from multiprocessing import Value
+
+import dash_core_components as dcc
+import dash_html_components as html
+import dash_flow_example
+import dash_dangerously_set_inner_html
+
+from dash import BaseDashView
+from dash.dependencies import Input, Output
+from dash.exceptions import PreventUpdate
+
+
+class DashView(BaseDashView):
+ def __init__(self, **kwargs):
+ super(DashView, self).__init__(**kwargs)
+
+ self.dash.config.suppress_callback_exceptions = True
+ self.dash.config.routes_pathname_prefix = '/dash/{}/'.format(self.dash_name)
+ self.dash.css.config.serve_locally = True
+ self.dash.scripts.config.serve_locally = True
+
+ def get(self, request, *args, **kwargs):
+ return self.serve_dash_index(request, self.dash_name, *args, **kwargs)
+
+ def _dash_component_suites(self, request, *args, **kwargs):
+ self.dash._generate_scripts_html()
+ self.dash._generate_css_dist_html()
+
+ return super(DashView, self)._dash_component_suites(request, *args, **kwargs)
+
+
+class DashSimpleCallback(DashView):
+ dash_name = 'dash01'
+
+ call_count = Value('i', 0)
+
+ def __init__(self, **kwargs):
+ super(DashSimpleCallback, self).__init__(**kwargs)
+
+ self.dash.layout = html.Div([
+ dcc.Input(
+ id='input',
+ value='initial value'
+ ),
+ html.Div(
+ html.Div([
+ 1.5,
+ None,
+ 'string',
+ html.Div(id='output-1')
+ ])
+ )
+ ])
+
+ self.dash.callback(Output('output-1', 'children'),
+ [Input('input', 'value')])(self.update_output)
+
+ def update_output(self, value):
+ self.call_count.value = self.call_count.value + 1
+
+ return value
+
+
+class DashWildcardCallback(DashView):
+ dash_name = 'dash02'
+
+ call_count = Value('i', 0)
+
+ def __init__(self, **kwargs):
+ super(DashWildcardCallback, self).__init__(**kwargs)
+
+ self.dash.layout = html.Div([
+ dcc.Input(
+ id='input',
+ value='initial value'
+ ),
+ html.Div(
+ html.Div([
+ 1.5,
+ None,
+ 'string',
+ html.Div(id='output-1', **{'data-cb': 'initial value',
+ 'aria-cb': 'initial value'})
+ ])
+ )
+ ])
+
+ self.dash.callback(Output('output-1', 'data-cb'),
+ [Input('input', 'value')])(self.update_data)
+ self.dash.callback(Output('output-1', 'children'),
+ [Input('output-1', 'data-cb')])(self.update_text)
+
+ def update_data(self, value):
+ self.call_count.value = self.call_count.value + 1
+
+ return value
+
+ def update_text(self, data):
+ return data
+
+
+class DashAbortedCallback(DashView):
+ dash_name = 'dash03'
+
+ initial_input = 'initial input'
+ initial_output = 'initial output'
+
+ callback1_count = Value('i', 0)
+ callback2_count = Value('i', 0)
+
+ def __init__(self, **kwargs):
+ super(DashAbortedCallback, self).__init__(**kwargs)
+
+ self.dash.layout = html.Div([
+ dcc.Input(id='input', value=self.initial_input),
+ html.Div(self.initial_output, id='output1'),
+ html.Div(self.initial_output, id='output2'),
+ ])
+
+ self.dash.callback(Output('output1', 'children'),
+ [Input('input', 'value')])(self.callback1)
+ self.dash.callback(Output('output2', 'children'),
+ [Input('output1', 'children')])(self.callback2)
+
+ def callback1(self, value):
+ self.callback1_count.value = self.callback1_count.value + 1
+ raise PreventUpdate("testing callback does not update")
+ return value
+
+ def callback2(self, value):
+ self.callback2_count.value = self.callback2_count.value + 1
+ return value
+
+
+class DashWildcardDataAttributes(DashView):
+ dash_name = 'dash04'
+
+ test_time = datetime.datetime(2012, 1, 10, 2, 3)
+ test_date = datetime.date(test_time.year, test_time.month, test_time.day)
+
+ def __init__(self, **kwargs):
+ super(DashWildcardDataAttributes, self).__init__(**kwargs)
+
+ self.dash.layout = html.Div([
+ html.Div(
+ id="inner-element",
+ **{
+ 'data-string': 'multiple words',
+ 'data-number': 512,
+ 'data-none': None,
+ 'data-date': self.test_date,
+ 'aria-progress': 5
+ }
+ )
+ ], id='data-element')
+
+
+class DashFlowComponent(DashView):
+ dash_name = 'dash05'
+
+ def __init__(self, **kwargs):
+ super(DashFlowComponent, self).__init__(**kwargs)
+
+ self.dash.layout = html.Div([
+ dash_flow_example.ExampleReactComponent(
+ id='react',
+ value='my-value',
+ label='react component'
+ ),
+ dash_flow_example.ExampleFlowComponent(
+ id='flow',
+ value='my-value',
+ label='flow component'
+ ),
+ html.Hr(),
+ html.Div(id='output')
+ ])
+
+ self.dash.callback(Output('output', 'children'),
+ [Input('react', 'value'),
+ Input('flow', 'value')])(self.display_output)
+
+ def display_output(self, react_value, flow_value):
+ return html.Div([
+ 'You have entered {} and {}'.format(react_value, flow_value),
+ html.Hr(),
+ html.Label('Flow Component Docstring'),
+ html.Pre(dash_flow_example.ExampleFlowComponent.__doc__),
+ html.Hr(),
+ html.Label('React PropTypes Component Docstring'),
+ html.Pre(dash_flow_example.ExampleReactComponent.__doc__),
+ html.Div(id='waitfor')
+ ])
+
+
+class DashNoPropsComponent(DashView):
+ dash_name = 'dash06'
+
+ def __init__(self, **kwargs):
+ super(DashNoPropsComponent, self).__init__(**kwargs)
+
+ self.dash.layout = html.Div([
+ dash_dangerously_set_inner_html.DangerouslySetInnerHTML('''
+ No Props Component
+ ''')
+ ])
+
+
+class DashMetaTags(DashView):
+ dash_name = 'dash07'
+
+ metas = [
+ {'name': 'description', 'content': 'my dash app'},
+ {'name': 'custom', 'content': 'customized'},
+ ]
+
+ def __init__(self, **kwargs):
+ super(DashMetaTags, self).__init__(dash_meta_tags=self.metas, **kwargs)
+
+ self.dash.layout = html.Div(id='content')
+
+
+class DashIndexCustomization(DashView):
+ dash_name = 'dash08'
+
+ def __init__(self, **kwargs):
+ super(DashIndexCustomization, self).__init__(**kwargs)
+
+ self.dash.index_string = '''
+
+
+
+ {%metas%}
+ {%title%}
+ {%favicon%}
+ {%css%}
+
+
+
+
+ {%app_entry%}
+
+ {%config%}
+ {%scripts%}
+
+
+
+
+
+ '''
+
+ self.dash.layout = html.Div('Dash app', id='app')
+
+
+class DashAssets(DashView):
+ dash_name = 'dash09'
+ dash_template = '''
+
+
+
+ {%metas%}
+ {%title%}
+ {%css%}
+
+
+
+ {%app_entry%}
+
+ {%config%}
+ {%scripts%}
+
+
+
+ '''
+ dash_assets_folder = 'dynamic_dash/assets'
+ dash_assets_ignore = '*ignored.*'
+
+ def __init__(self, **kwargs):
+ super(DashAssets, self).__init__(**kwargs)
+
+ self.dash.layout = html.Div([html.Div(id='content'), dcc.Input(id='test')], id='layout')
+
+
+class DashInvalidIndexString(DashView):
+ dash_name = 'dash10'
+
+ def __init__(self, **kwargs):
+ super(DashInvalidIndexString, self).__init__(**kwargs)
+
+ self.dash.layout = html.Div()
+
+
+
+class DashExternalFilesInit(DashView):
+ dash_name = 'dash11'
+
+ js_files = [
+ 'https://www.google-analytics.com/analytics.js',
+ {'src': 'https://cdn.polyfill.io/v2/polyfill.min.js'},
+ {
+ 'src': 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js',
+ 'integrity': 'sha256-YN22NHB7zs5+LjcHWgk3zL0s+CRnzCQzDOFnndmUamY=',
+ 'crossorigin': 'anonymous'
+ },
+ {
+ 'src': 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js',
+ 'integrity': 'sha256-VKITM616rVzV+MI3kZMNUDoY5uTsuSl1ZvEeZhNoJVk=',
+ 'crossorigin': 'anonymous'
+ }
+ ]
+
+ css_files = [
+ 'https://codepen.io/chriddyp/pen/bWLwgP.css',
+ {
+ 'href': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css',
+ 'rel': 'stylesheet',
+ 'integrity': 'sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO',
+ 'crossorigin': 'anonymous'
+ }
+ ]
+
+ def __init__(self, **kwargs):
+ super(DashExternalFilesInit, self).__init__(dash_external_scripts=self.js_files,
+ dash_external_stylesheets=self.css_files, **kwargs)
+
+ self.dash.index_string = '''
+
+
+
+ {%metas%}
+ {%title%}
+ {%css%}
+
+
+
+ Hello World
+ Btn
+ {%app_entry%}
+
+ {%config%}
+ {%scripts%}
+
+
+
+ '''
+
+ self.dash.layout = html.Div()
+
+
+class DashFuncLayoutAccepted(DashView):
+ dash_name = 'dash12'
+
+ def __init__(self, **kwargs):
+ super(DashFuncLayoutAccepted, self).__init__(**kwargs)
+
+ def create_layout():
+ return html.Div('Hello World')
+
+ self.dash.layout = create_layout
diff --git a/tests/django_project/manage.py b/tests/django_project/manage.py
new file mode 100644
index 0000000..f522e00
--- /dev/null
+++ b/tests/django_project/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError:
+ # The above import may fail for some other reason. Ensure that the
+ # issue is really that Django is missing to avoid masking other
+ # exceptions on Python 2.
+ try:
+ import django
+ except ImportError:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ )
+ raise
+ execute_from_command_line(sys.argv)
diff --git a/tests/django_project/project/__init__.py b/tests/django_project/project/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/django_project/project/settings.py b/tests/django_project/project/settings.py
new file mode 100644
index 0000000..c020b4e
--- /dev/null
+++ b/tests/django_project/project/settings.py
@@ -0,0 +1,87 @@
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'a8nlqf7c!k_glgd2xsdz4=&^q53_g9#4zks=nf3&3u@4!^0^i-'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+
+ 'dynamic_dash.apps.App',
+ 'static_dash.apps.App',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'project.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')]
+ ,
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'project.wsgi.application'
+
+
+# Database
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ }
+}
+
+
+# Internationalization
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+
+STATIC_URL = '/static/'
+
+STATIC_ROOT = os.path.join(BASE_DIR, 'static')
diff --git a/tests/django_project/project/urls.py b/tests/django_project/project/urls.py
new file mode 100644
index 0000000..f950721
--- /dev/null
+++ b/tests/django_project/project/urls.py
@@ -0,0 +1,15 @@
+from django.conf.urls import url, include
+from django.conf import settings
+from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+
+
+urlpatterns = [
+ url(r'^dash/', include('dash.urls')),
+]
+
+
+if settings.DEBUG:
+ from django.conf.urls.static import static
+
+ urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+ # urlpatterns += staticfiles_urlpatterns()
diff --git a/tests/django_project/project/wsgi.py b/tests/django_project/project/wsgi.py
new file mode 100644
index 0000000..2ef9a16
--- /dev/null
+++ b/tests/django_project/project/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for project project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
+
+application = get_wsgi_application()
diff --git a/tests/django_project/static/dynamic_dash/assets/load_first.js b/tests/django_project/static/dynamic_dash/assets/load_first.js
new file mode 100644
index 0000000..22c7799
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/load_first.js
@@ -0,0 +1,7 @@
+window.tested = ['load_first'];
+var ramdaTest = document.getElementById('ramda-test');
+if (ramdaTest) {
+ ramdaTest.innerHTML = R.join(' ', R.concat(['hello'], ['world']).map(function(x) {
+ return _.capitalize(x);
+ }));
+}
diff --git a/tests/django_project/static/dynamic_dash/assets/load_ignored.js b/tests/django_project/static/dynamic_dash/assets/load_ignored.js
new file mode 100644
index 0000000..668e477
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/load_ignored.js
@@ -0,0 +1 @@
+window.tested = 'IGNORED'; // Break the chain.
\ No newline at end of file
diff --git a/tests/django_project/static/dynamic_dash/assets/nested_css/nested.css b/tests/django_project/static/dynamic_dash/assets/nested_css/nested.css
new file mode 100644
index 0000000..24ba3d2
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/nested_css/nested.css
@@ -0,0 +1,3 @@
+#content {
+ padding: 8px;
+}
\ No newline at end of file
diff --git a/tests/django_project/static/dynamic_dash/assets/nested_js/load_after.js b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after.js
new file mode 100644
index 0000000..6f520fd
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after.js
@@ -0,0 +1 @@
+window.tested.push('load_after');
\ No newline at end of file
diff --git a/tests/django_project/static/dynamic_dash/assets/nested_js/load_after1.js b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after1.js
new file mode 100644
index 0000000..1629d39
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after1.js
@@ -0,0 +1 @@
+window.tested.push('load_after1');
diff --git a/tests/django_project/static/dynamic_dash/assets/nested_js/load_after10.js b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after10.js
new file mode 100644
index 0000000..fcfdb59
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after10.js
@@ -0,0 +1 @@
+window.tested.push('load_after10');
\ No newline at end of file
diff --git a/tests/django_project/static/dynamic_dash/assets/nested_js/load_after11.js b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after11.js
new file mode 100644
index 0000000..bd11cd2
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after11.js
@@ -0,0 +1 @@
+window.tested.push('load_after11');
\ No newline at end of file
diff --git a/tests/django_project/static/dynamic_dash/assets/nested_js/load_after2.js b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after2.js
new file mode 100644
index 0000000..0b76a55
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after2.js
@@ -0,0 +1 @@
+window.tested.push('load_after2');
\ No newline at end of file
diff --git a/tests/django_project/static/dynamic_dash/assets/nested_js/load_after3.js b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after3.js
new file mode 100644
index 0000000..d913af9
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after3.js
@@ -0,0 +1 @@
+window.tested.push('load_after3');
\ No newline at end of file
diff --git a/tests/django_project/static/dynamic_dash/assets/nested_js/load_after4.js b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after4.js
new file mode 100644
index 0000000..2507e38
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/nested_js/load_after4.js
@@ -0,0 +1 @@
+window.tested.push('load_after4');
\ No newline at end of file
diff --git a/tests/django_project/static/dynamic_dash/assets/nested_js/load_last.js b/tests/django_project/static/dynamic_dash/assets/nested_js/load_last.js
new file mode 100644
index 0000000..285aa60
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/nested_js/load_last.js
@@ -0,0 +1 @@
+document.getElementById('tested').innerHTML = JSON.stringify(window.tested);
diff --git a/tests/django_project/static/dynamic_dash/assets/reset.css b/tests/django_project/static/dynamic_dash/assets/reset.css
new file mode 100644
index 0000000..c7c7b58
--- /dev/null
+++ b/tests/django_project/static/dynamic_dash/assets/reset.css
@@ -0,0 +1,2 @@
+body {margin: 0;}
+button {height: 18px}
diff --git a/tests/django_project/static/static_dash/assets/load_first.js b/tests/django_project/static/static_dash/assets/load_first.js
new file mode 100644
index 0000000..22c7799
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/load_first.js
@@ -0,0 +1,7 @@
+window.tested = ['load_first'];
+var ramdaTest = document.getElementById('ramda-test');
+if (ramdaTest) {
+ ramdaTest.innerHTML = R.join(' ', R.concat(['hello'], ['world']).map(function(x) {
+ return _.capitalize(x);
+ }));
+}
diff --git a/tests/django_project/static/static_dash/assets/load_ignored.js b/tests/django_project/static/static_dash/assets/load_ignored.js
new file mode 100644
index 0000000..668e477
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/load_ignored.js
@@ -0,0 +1 @@
+window.tested = 'IGNORED'; // Break the chain.
\ No newline at end of file
diff --git a/tests/django_project/static/static_dash/assets/nested_css/nested.css b/tests/django_project/static/static_dash/assets/nested_css/nested.css
new file mode 100644
index 0000000..24ba3d2
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/nested_css/nested.css
@@ -0,0 +1,3 @@
+#content {
+ padding: 8px;
+}
\ No newline at end of file
diff --git a/tests/django_project/static/static_dash/assets/nested_js/load_after.js b/tests/django_project/static/static_dash/assets/nested_js/load_after.js
new file mode 100644
index 0000000..6f520fd
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/nested_js/load_after.js
@@ -0,0 +1 @@
+window.tested.push('load_after');
\ No newline at end of file
diff --git a/tests/django_project/static/static_dash/assets/nested_js/load_after1.js b/tests/django_project/static/static_dash/assets/nested_js/load_after1.js
new file mode 100644
index 0000000..1629d39
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/nested_js/load_after1.js
@@ -0,0 +1 @@
+window.tested.push('load_after1');
diff --git a/tests/django_project/static/static_dash/assets/nested_js/load_after10.js b/tests/django_project/static/static_dash/assets/nested_js/load_after10.js
new file mode 100644
index 0000000..fcfdb59
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/nested_js/load_after10.js
@@ -0,0 +1 @@
+window.tested.push('load_after10');
\ No newline at end of file
diff --git a/tests/django_project/static/static_dash/assets/nested_js/load_after11.js b/tests/django_project/static/static_dash/assets/nested_js/load_after11.js
new file mode 100644
index 0000000..bd11cd2
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/nested_js/load_after11.js
@@ -0,0 +1 @@
+window.tested.push('load_after11');
\ No newline at end of file
diff --git a/tests/django_project/static/static_dash/assets/nested_js/load_after2.js b/tests/django_project/static/static_dash/assets/nested_js/load_after2.js
new file mode 100644
index 0000000..0b76a55
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/nested_js/load_after2.js
@@ -0,0 +1 @@
+window.tested.push('load_after2');
\ No newline at end of file
diff --git a/tests/django_project/static/static_dash/assets/nested_js/load_after3.js b/tests/django_project/static/static_dash/assets/nested_js/load_after3.js
new file mode 100644
index 0000000..d913af9
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/nested_js/load_after3.js
@@ -0,0 +1 @@
+window.tested.push('load_after3');
\ No newline at end of file
diff --git a/tests/django_project/static/static_dash/assets/nested_js/load_after4.js b/tests/django_project/static/static_dash/assets/nested_js/load_after4.js
new file mode 100644
index 0000000..2507e38
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/nested_js/load_after4.js
@@ -0,0 +1 @@
+window.tested.push('load_after4');
\ No newline at end of file
diff --git a/tests/django_project/static/static_dash/assets/nested_js/load_last.js b/tests/django_project/static/static_dash/assets/nested_js/load_last.js
new file mode 100644
index 0000000..285aa60
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/nested_js/load_last.js
@@ -0,0 +1 @@
+document.getElementById('tested').innerHTML = JSON.stringify(window.tested);
diff --git a/tests/django_project/static/static_dash/assets/reset.css b/tests/django_project/static/static_dash/assets/reset.css
new file mode 100644
index 0000000..c7c7b58
--- /dev/null
+++ b/tests/django_project/static/static_dash/assets/reset.css
@@ -0,0 +1,2 @@
+body {margin: 0;}
+button {height: 18px}
diff --git a/tests/django_project/static_dash/__init__.py b/tests/django_project/static_dash/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/django_project/static_dash/apps.py b/tests/django_project/static_dash/apps.py
new file mode 100644
index 0000000..44d89da
--- /dev/null
+++ b/tests/django_project/static_dash/apps.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.apps import AppConfig
+
+# Initialise views for registering in dash.dash.BaseDashView._dashes
+from .views import *
+
+
+class App(AppConfig):
+ name = 'static_dash'
diff --git a/tests/django_project/static_dash/migrations/__init__.py b/tests/django_project/static_dash/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/django_project/static_dash/models.py b/tests/django_project/static_dash/models.py
new file mode 100644
index 0000000..1dfab76
--- /dev/null
+++ b/tests/django_project/static_dash/models.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models
+
+# Create your models here.
diff --git a/tests/django_project/static_dash/static/static_dash/assets/load_first.js b/tests/django_project/static_dash/static/static_dash/assets/load_first.js
new file mode 100644
index 0000000..22c7799
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/load_first.js
@@ -0,0 +1,7 @@
+window.tested = ['load_first'];
+var ramdaTest = document.getElementById('ramda-test');
+if (ramdaTest) {
+ ramdaTest.innerHTML = R.join(' ', R.concat(['hello'], ['world']).map(function(x) {
+ return _.capitalize(x);
+ }));
+}
diff --git a/tests/django_project/static_dash/static/static_dash/assets/load_ignored.js b/tests/django_project/static_dash/static/static_dash/assets/load_ignored.js
new file mode 100644
index 0000000..668e477
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/load_ignored.js
@@ -0,0 +1 @@
+window.tested = 'IGNORED'; // Break the chain.
\ No newline at end of file
diff --git a/tests/django_project/static_dash/static/static_dash/assets/nested_css/nested.css b/tests/django_project/static_dash/static/static_dash/assets/nested_css/nested.css
new file mode 100644
index 0000000..24ba3d2
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/nested_css/nested.css
@@ -0,0 +1,3 @@
+#content {
+ padding: 8px;
+}
\ No newline at end of file
diff --git a/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after.js b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after.js
new file mode 100644
index 0000000..6f520fd
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after.js
@@ -0,0 +1 @@
+window.tested.push('load_after');
\ No newline at end of file
diff --git a/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after1.js b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after1.js
new file mode 100644
index 0000000..1629d39
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after1.js
@@ -0,0 +1 @@
+window.tested.push('load_after1');
diff --git a/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after10.js b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after10.js
new file mode 100644
index 0000000..fcfdb59
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after10.js
@@ -0,0 +1 @@
+window.tested.push('load_after10');
\ No newline at end of file
diff --git a/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after11.js b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after11.js
new file mode 100644
index 0000000..bd11cd2
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after11.js
@@ -0,0 +1 @@
+window.tested.push('load_after11');
\ No newline at end of file
diff --git a/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after2.js b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after2.js
new file mode 100644
index 0000000..0b76a55
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after2.js
@@ -0,0 +1 @@
+window.tested.push('load_after2');
\ No newline at end of file
diff --git a/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after3.js b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after3.js
new file mode 100644
index 0000000..d913af9
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after3.js
@@ -0,0 +1 @@
+window.tested.push('load_after3');
\ No newline at end of file
diff --git a/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after4.js b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after4.js
new file mode 100644
index 0000000..2507e38
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_after4.js
@@ -0,0 +1 @@
+window.tested.push('load_after4');
\ No newline at end of file
diff --git a/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_last.js b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_last.js
new file mode 100644
index 0000000..285aa60
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/nested_js/load_last.js
@@ -0,0 +1 @@
+document.getElementById('tested').innerHTML = JSON.stringify(window.tested);
diff --git a/tests/django_project/static_dash/static/static_dash/assets/reset.css b/tests/django_project/static_dash/static/static_dash/assets/reset.css
new file mode 100644
index 0000000..c7c7b58
--- /dev/null
+++ b/tests/django_project/static_dash/static/static_dash/assets/reset.css
@@ -0,0 +1,2 @@
+body {margin: 0;}
+button {height: 18px}
diff --git a/tests/django_project/static_dash/views.py b/tests/django_project/static_dash/views.py
new file mode 100644
index 0000000..86a2d56
--- /dev/null
+++ b/tests/django_project/static_dash/views.py
@@ -0,0 +1,371 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import datetime
+from multiprocessing import Value
+
+import dash_core_components as dcc
+import dash_html_components as html
+import dash_flow_example
+import dash_dangerously_set_inner_html
+
+from dash import BaseDashView, Dash
+from dash.dependencies import Input, Output
+from dash.exceptions import PreventUpdate
+
+
+class DashView(BaseDashView):
+ @staticmethod
+ def set_config(dash, dash_name):
+ dash.config.suppress_callback_exceptions = True
+ dash.config.routes_pathname_prefix = '/dash/{}/'.format(dash_name)
+ dash.css.config.serve_locally = True
+ dash.scripts.config.serve_locally = True
+
+ def get(self, request, *args, **kwargs):
+ return self.serve_dash_index(request, self.dash_name, *args, **kwargs)
+
+
+class DashSimpleCallback(DashView):
+ dash_name = 'static_dash01'
+
+ dash = Dash()
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div([
+ dcc.Input(
+ id='input',
+ value='initial value'
+ ),
+ html.Div(
+ html.Div([
+ 1.5,
+ None,
+ 'string',
+ html.Div(id='output-1')
+ ])
+ )
+ ])
+
+ call_count = Value('i', 0)
+
+ @staticmethod
+ @dash.callback(Output('output-1', 'children'), [Input('input', 'value')])
+ def update_output(value):
+ DashSimpleCallback.call_count.value = DashSimpleCallback.call_count.value + 1
+
+ return value
+
+
+class DashWildcardCallback(DashView):
+ dash_name = 'static_dash02'
+
+ dash = Dash()
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div([
+ dcc.Input(
+ id='input',
+ value='initial value'
+ ),
+ html.Div(
+ html.Div([
+ 1.5,
+ None,
+ 'string',
+ html.Div(id='output-1', **{'data-cb': 'initial value',
+ 'aria-cb': 'initial value'})
+ ])
+ )
+ ])
+
+ call_count = Value('i', 0)
+
+ @classmethod # As an opportunity
+ def update_data(cls, value):
+ cls.call_count.value = cls.call_count.value + 1
+
+ return value
+
+ @staticmethod # As an opportunity
+ @dash.callback(Output('output-1', 'children'), [Input('output-1', 'data-cb')])
+ def update_text(data):
+ return data
+
+DashWildcardCallback.dash.callback(Output('output-1', 'data-cb'),
+ [Input('input', 'value')])(DashWildcardCallback.update_data)
+
+
+class DashAbortedCallback(DashView):
+ dash_name = 'static_dash03'
+
+ initial_input = 'initial input'
+ initial_output = 'initial output'
+
+ dash = Dash()
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div([
+ dcc.Input(id='input', value=initial_input),
+ html.Div(initial_output, id='output1'),
+ html.Div(initial_output, id='output2'),
+ ])
+
+ callback1_count = Value('i', 0)
+ callback2_count = Value('i', 0)
+
+ @classmethod # As an opportunity
+ def callback1(cls, value):
+ cls.callback1_count.value = cls.callback1_count.value + 1
+ raise PreventUpdate("testing callback does not update")
+ return value
+
+ @staticmethod # As an opportunity
+ @dash.callback(Output('output2', 'children'), [Input('output1', 'children')])
+ def callback2(value):
+ DashAbortedCallback.callback2_count.value = DashAbortedCallback.callback2_count.value + 1
+ return value
+
+DashAbortedCallback.dash.callback(Output('output1', 'children'),
+ [Input('input', 'value')])(DashAbortedCallback.callback1)
+
+
+class DashWildcardDataAttributes(DashView):
+ dash_name = 'static_dash04'
+
+ test_time = datetime.datetime(2012, 1, 10, 2, 3)
+ test_date = datetime.date(test_time.year, test_time.month, test_time.day)
+
+ dash = Dash()
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div([
+ html.Div(
+ id="inner-element",
+ **{
+ 'data-string': 'multiple words',
+ 'data-number': 512,
+ 'data-none': None,
+ 'data-date': test_date,
+ 'aria-progress': 5
+ }
+ )
+ ], id='data-element')
+
+
+class DashFlowComponent(DashView):
+ dash_name = 'static_dash05'
+
+ dash = Dash()
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div([
+ dash_flow_example.ExampleReactComponent(
+ id='react',
+ value='my-value',
+ label='react component'
+ ),
+ dash_flow_example.ExampleFlowComponent(
+ id='flow',
+ value='my-value',
+ label='flow component'
+ ),
+ html.Hr(),
+ html.Div(id='output')
+ ])
+
+ @staticmethod
+ @dash.callback(Output('output', 'children'),
+ [Input('react', 'value'), Input('flow', 'value')])
+ def display_output(react_value, flow_value):
+ return html.Div([
+ 'You have entered {} and {}'.format(react_value, flow_value),
+ html.Hr(),
+ html.Label('Flow Component Docstring'),
+ html.Pre(dash_flow_example.ExampleFlowComponent.__doc__),
+ html.Hr(),
+ html.Label('React PropTypes Component Docstring'),
+ html.Pre(dash_flow_example.ExampleReactComponent.__doc__),
+ html.Div(id='waitfor')
+ ])
+
+
+class DashNoPropsComponent(DashView):
+ dash_name = 'static_dash06'
+
+ dash = Dash()
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div([
+ dash_dangerously_set_inner_html.DangerouslySetInnerHTML('''
+ No Props Component
+ ''')
+ ])
+
+
+class DashMetaTags(DashView):
+ dash_name = 'static_dash07'
+
+ metas = [
+ {'name': 'description', 'content': 'my dash app'},
+ {'name': 'custom', 'content': 'customized'},
+ ]
+
+ dash = Dash(meta_tags=metas)
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div(id='content')
+
+
+class DashIndexCustomization(DashView):
+ dash_name = 'static_dash08'
+
+ dash = Dash()
+ dash.index_string = '''
+
+
+
+ {%metas%}
+ {%title%}
+ {%favicon%}
+ {%css%}
+
+
+
+
+ {%app_entry%}
+
+ {%config%}
+ {%scripts%}
+
+
+
+
+
+ '''
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div('Dash app', id='app')
+
+
+class DashAssets(DashView):
+ dash_name = 'static_dash09'
+ dash_template = '''
+
+
+
+ {%metas%}
+ {%title%}
+ {%css%}
+
+
+
+ {%app_entry%}
+
+ {%config%}
+ {%scripts%}
+
+
+
+ '''
+ dash_assets_folder = 'static_dash/assets'
+ dash_assets_ignore = '*ignored.*'
+
+ dash = Dash()
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div([html.Div(id='content'), dcc.Input(id='test')], id='layout')
+
+
+class DashInvalidIndexString(DashView):
+ dash_name = 'static_dash10'
+
+ dash = Dash()
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div()
+
+
+class DashExternalFilesInit(DashView):
+ dash_name = 'static_dash11'
+
+ js_files = [
+ 'https://www.google-analytics.com/analytics.js',
+ {'src': 'https://cdn.polyfill.io/v2/polyfill.min.js'},
+ {
+ 'src': 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js',
+ 'integrity': 'sha256-YN22NHB7zs5+LjcHWgk3zL0s+CRnzCQzDOFnndmUamY=',
+ 'crossorigin': 'anonymous'
+ },
+ {
+ 'src': 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js',
+ 'integrity': 'sha256-VKITM616rVzV+MI3kZMNUDoY5uTsuSl1ZvEeZhNoJVk=',
+ 'crossorigin': 'anonymous'
+ }
+ ]
+
+ css_files = [
+ 'https://codepen.io/chriddyp/pen/bWLwgP.css',
+ {
+ 'href': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css',
+ 'rel': 'stylesheet',
+ 'integrity': 'sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO',
+ 'crossorigin': 'anonymous'
+ }
+ ]
+
+ dash = Dash(external_scripts=js_files, external_stylesheets=css_files)
+ dash.index_string = '''
+
+
+
+ {%metas%}
+ {%title%}
+ {%css%}
+
+
+
+ Hello World
+ Btn
+ {%app_entry%}
+
+ {%config%}
+ {%scripts%}
+
+
+
+ '''
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = html.Div()
+
+
+class DashFuncLayoutAccepted(DashView):
+ dash_name = 'static_dash12'
+
+ dash = Dash()
+
+ DashView.set_config(dash, dash_name) # As an opportunity
+
+ dash.layout = lambda: html.Div('Hello World')
diff --git a/tests/django_project/tests/IntegrationTests.py b/tests/django_project/tests/IntegrationTests.py
new file mode 100644
index 0000000..f90f9a4
--- /dev/null
+++ b/tests/django_project/tests/IntegrationTests.py
@@ -0,0 +1,61 @@
+import sys
+import time
+
+from selenium import webdriver
+from django.test import LiveServerTestCase
+
+from .utils import invincible, wait_for
+
+
+class IntegrationTests(LiveServerTestCase):
+ def wait_for_element_by_id(self, id):
+ wait_for(lambda: None is not invincible(
+ lambda: self.driver.find_element_by_id(id)
+ ), timeout=5)
+ return self.driver.find_element_by_id(id)
+
+ @classmethod
+ def setUpClass(cls):
+ super(IntegrationTests, cls).setUpClass()
+
+ cls.driver = webdriver.Chrome()
+
+ @classmethod
+ def tearDownClass(cls):
+ super(IntegrationTests, cls).tearDownClass()
+
+ cls.driver.quit()
+
+ def tearDown(self):
+ time.sleep(2)
+
+ def open(self, dash):
+ # Visit the dash page
+ self.driver.get('{}/{}'.format(self.live_server_url, dash))
+ time.sleep(1)
+
+ # Inject an error and warning logger
+ logger = '''
+ window.tests = {};
+ window.tests.console = {error: [], warn: [], log: []};
+
+ var _log = console.log;
+ var _warn = console.warn;
+ var _error = console.error;
+
+ console.log = function() {
+ window.tests.console.log.push({method: 'log', arguments: arguments});
+ return _log.apply(console, arguments);
+ };
+
+ console.warn = function() {
+ window.tests.console.warn.push({method: 'warn', arguments: arguments});
+ return _warn.apply(console, arguments);
+ };
+
+ console.error = function() {
+ window.tests.console.error.push({method: 'error', arguments: arguments});
+ return _error.apply(console, arguments);
+ };
+ '''
+ self.driver.execute_script(logger)
diff --git a/tests/django_project/tests/__init__.py b/tests/django_project/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/django_project/tests/test_integration.py b/tests/django_project/tests/test_integration.py
new file mode 100644
index 0000000..6ea8482
--- /dev/null
+++ b/tests/django_project/tests/test_integration.py
@@ -0,0 +1,294 @@
+import itertools
+import re
+import json
+
+import dash_html_components as html
+
+from .IntegrationTests import IntegrationTests
+from .utils import assert_clean_console, wait_for
+
+from ..dynamic_dash import views as dynamic_views
+from ..static_dash import views as static_views
+
+
+class Tests(IntegrationTests):
+ def _simple_callback(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ output1 = self.wait_for_element_by_id('output-1')
+ wait_for(lambda: output1.text == 'initial value')
+
+ input1 = self.wait_for_element_by_id('input')
+ input1.clear()
+
+ input1.send_keys('hello world')
+
+ output1 = lambda: self.wait_for_element_by_id('output-1')
+ wait_for(lambda: output1().text == 'hello world')
+
+ self.assertEqual(
+ view_class.call_count.value,
+ # an initial call to retrieve the first value
+ 1 +
+ # one for each hello world character
+ len('hello world')
+ )
+
+ assert_clean_console(self)
+
+ def _wildcard_callback(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ output1 = self.wait_for_element_by_id('output-1')
+ wait_for(lambda: output1.text == 'initial value')
+
+ input1 = self.wait_for_element_by_id('input')
+ input1.clear()
+
+ input1.send_keys('hello world')
+
+ output1 = lambda: self.wait_for_element_by_id('output-1')
+ wait_for(lambda: output1().text == 'hello world')
+
+ self.assertEqual(
+ view_class.call_count.value,
+ # an initial call
+ 1 +
+ # one for each hello world character
+ len('hello world')
+ )
+
+ assert_clean_console(self)
+
+ def _aborted_callback(self, view_class):
+ """Raising PreventUpdate prevents update and triggering dependencies
+ """
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ input_ = self.wait_for_element_by_id('input')
+ input_.clear()
+ input_.send_keys('x')
+ output1 = self.wait_for_element_by_id('output1')
+ output2 = self.wait_for_element_by_id('output2')
+
+ # callback1 runs twice (initial page load and through send_keys)
+ self.assertEqual(view_class.callback1_count.value, 2)
+
+ # callback2 is never triggered, even on initial load
+ self.assertEqual(view_class.callback2_count.value, 0)
+
+ # double check that output1 and output2 children were not updated
+ self.assertEqual(output1.text, view_class.initial_output)
+ self.assertEqual(output2.text, view_class.initial_output)
+
+ assert_clean_console(self)
+
+ def _wildcard_data_attributes(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ div = self.wait_for_element_by_id('data-element')
+
+ # React wraps text and numbers with e.g.
+ # Remove those
+ comment_regex = ''
+
+ # Somehow the html attributes are unordered.
+ # Try different combinations (they're all valid html)
+ permutations = itertools.permutations([
+ 'id="inner-element"',
+ 'data-string="multiple words"',
+ 'data-number="512"',
+ 'data-date="%s"' % (view_class.test_date),
+ 'aria-progress="5"'
+ ], 5)
+ passed = False
+ for permutation in permutations:
+ actual_cleaned = re.sub(comment_regex, '',
+ div.get_attribute('innerHTML'))
+ expected_cleaned = re.sub(
+ comment_regex,
+ '',
+ "
"
+ .replace('PERMUTE', ' '.join(list(permutation)))
+ )
+ passed = passed or (actual_cleaned == expected_cleaned)
+ if passed:
+ break
+
+ if not passed:
+ raise Exception(
+ 'HTML does not match\nActual:\n{}\n\nExpected:\n{}'.format(
+ actual_cleaned,
+ expected_cleaned
+ )
+ )
+
+ assert_clean_console(self)
+
+ def _flow_component(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ self.wait_for_element_by_id('waitfor')
+
+ def _no_props_component(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ assert_clean_console(self)
+
+ def _meta_tags(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ metas = view_class.metas
+ meta = self.driver.find_elements_by_tag_name('meta')
+
+ # -2 for the meta charset and http-equiv.
+ self.assertEqual(len(metas), len(meta) - 2, 'Not enough meta tags')
+
+ for i in range(2, len(meta)):
+ meta_tag = meta[i]
+ meta_info = metas[i - 2]
+ name = meta_tag.get_attribute('name')
+ content = meta_tag.get_attribute('content')
+ self.assertEqual(name, meta_info['name'])
+ self.assertEqual(content, meta_info['content'])
+
+ def _index_customization(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ header = self.wait_for_element_by_id('custom-header')
+ footer = self.wait_for_element_by_id('custom-footer')
+
+ self.assertEqual('My custom header', header.text)
+ self.assertEqual('My custom footer', footer.text)
+
+ add = self.wait_for_element_by_id('add')
+
+ self.assertEqual('Got added', add.text)
+
+ def _assets(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ body = self.driver.find_element_by_tag_name('body')
+
+ body_margin = body.value_of_css_property('margin')
+ self.assertEqual('0px', body_margin)
+
+ content = self.wait_for_element_by_id('content')
+ content_padding = content.value_of_css_property('padding')
+ self.assertEqual('8px', content_padding)
+
+ tested = self.wait_for_element_by_id('tested')
+ tested = json.loads(tested.text)
+
+ order = ('load_first', 'load_after', 'load_after1',
+ 'load_after10', 'load_after11', 'load_after2',
+ 'load_after3', 'load_after4', )
+
+ self.assertEqual(len(order), len(tested))
+
+ for i in range(len(tested)):
+ self.assertEqual(order[i], tested[i])
+
+ def _invalid_index_string(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ app = view_class().dash
+
+ def will_raise():
+ app.index_string = '''
+
+
+
+ {%metas%}
+ {%title%}
+ {%favicon%}
+ {%css%}
+
+
+
+
+
+
+ '''
+
+ with self.assertRaises(Exception) as context:
+ will_raise()
+
+ app.layout = html.Div()
+
+ exc_msg = str(context.exception)
+ self.assertTrue('{%app_entry%}' in exc_msg)
+ self.assertTrue('{%config%}' in exc_msg)
+ self.assertTrue('{%scripts%}' in exc_msg)
+
+ def _external_files_init(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ js_urls = [x['src'] if isinstance(x, dict) else x for x in view_class.js_files]
+ css_urls = [x['href'] if isinstance(x, dict) else x for x in view_class.css_files]
+
+ for fmt, url in itertools.chain((("//script[@src='{}']", x) for x in js_urls),
+ (("//link[@href='{}']", x) for x in css_urls)):
+ self.driver.find_element_by_xpath(fmt.format(url))
+
+ # Ensure the button style was overloaded by reset (set to 38px in codepen)
+ btn = self.driver.find_element_by_id('btn')
+ btn_height = btn.value_of_css_property('height')
+
+ self.assertEqual('38px', btn_height)
+
+ # ensure ramda was loaded before the assets so they can use it.
+ lo_test = self.driver.find_element_by_id('ramda-test')
+ self.assertEqual('Hello World', lo_test.text)
+
+ def _func_layout_accepted(self, view_class):
+ self.open('dash/{}/'.format(view_class.dash_name))
+
+ def test_simple_callback(self):
+ self._simple_callback(dynamic_views.DashSimpleCallback)
+ self._simple_callback(static_views.DashSimpleCallback)
+
+ def test_wildcard_callback(self):
+ self._wildcard_callback(dynamic_views.DashWildcardCallback)
+ self._wildcard_callback(static_views.DashWildcardCallback)
+
+ def test_aborted_callback(self):
+ self._aborted_callback(dynamic_views.DashAbortedCallback)
+ self._aborted_callback(static_views.DashAbortedCallback)
+
+ def test_wildcard_data_attributes(self):
+ self._wildcard_data_attributes(dynamic_views.DashWildcardDataAttributes)
+ self._wildcard_data_attributes(static_views.DashWildcardDataAttributes)
+
+ def test_flow_component(self):
+ self._flow_component(dynamic_views.DashFlowComponent)
+ self._flow_component(static_views.DashFlowComponent)
+
+ def test_no_props_component(self):
+ self._no_props_component(dynamic_views.DashNoPropsComponent)
+ self._no_props_component(static_views.DashNoPropsComponent)
+
+ def test_meta_tags(self):
+ self._meta_tags(dynamic_views.DashMetaTags)
+ self._meta_tags(static_views.DashMetaTags)
+
+ def test_index_customization(self):
+ self._index_customization(dynamic_views.DashIndexCustomization)
+ self._index_customization(static_views.DashIndexCustomization)
+
+ def test_assets(self):
+ self._assets(dynamic_views.DashAssets)
+ self._assets(static_views.DashAssets)
+
+ def test_invalid_index_string(self):
+ self._invalid_index_string(dynamic_views.DashInvalidIndexString)
+ self._invalid_index_string(static_views.DashInvalidIndexString)
+
+ def test_external_files_init(self):
+ self._external_files_init(dynamic_views.DashExternalFilesInit)
+ self._external_files_init(static_views.DashExternalFilesInit)
+
+ def test_func_layout_accepted(self):
+ self._func_layout_accepted(dynamic_views.DashFuncLayoutAccepted)
+ self._func_layout_accepted(static_views.DashFuncLayoutAccepted)
diff --git a/tests/django_project/tests/utils.py b/tests/django_project/tests/utils.py
new file mode 100644
index 0000000..e817ecd
--- /dev/null
+++ b/tests/django_project/tests/utils.py
@@ -0,0 +1,82 @@
+import time
+
+
+TIMEOUT = 5 # Seconds
+
+
+def invincible(func):
+ def wrap():
+ try:
+ return func()
+ except:
+ pass
+ return wrap
+
+
+class WaitForTimeout(Exception):
+ """This should only be raised inside the `wait_for` function."""
+ pass
+
+
+def wait_for(condition_function, get_message=lambda: '', *args, **kwargs):
+ """
+ Waits for condition_function to return True or raises WaitForTimeout.
+ :param (function) condition_function: Should return True on success.
+ :param args: Optional args to pass to condition_function.
+ :param kwargs: Optional kwargs to pass to condition_function.
+ if `timeout` is in kwargs, it will be used to override TIMEOUT
+ :raises: WaitForTimeout If condition_function doesn't return True in time.
+ Usage:
+ def get_element(selector):
+ # some code to get some element or return a `False`-y value.
+ selector = '.js-plotly-plot'
+ try:
+ wait_for(get_element, selector)
+ except WaitForTimeout:
+ self.fail('element never appeared...')
+ plot = get_element(selector) # we know it exists.
+ """
+ def wrapped_condition_function():
+ """We wrap this to alter the call base on the closure."""
+ if args and kwargs:
+ return condition_function(*args, **kwargs)
+ if args:
+ return condition_function(*args)
+ if kwargs:
+ return condition_function(**kwargs)
+ return condition_function()
+
+ if 'timeout' in kwargs:
+ timeout = kwargs['timeout']
+ del kwargs['timeout']
+ else:
+ timeout = TIMEOUT
+
+ start_time = time.time()
+ while time.time() < start_time + timeout:
+ if wrapped_condition_function():
+ return True
+ time.sleep(0.5)
+
+ raise WaitForTimeout(get_message())
+
+
+def assert_clean_console(TestClass):
+ def assert_no_console_errors(TestClass):
+ TestClass.assertEqual(
+ TestClass.driver.execute_script(
+ 'return window.tests.console.error.length'
+ ),
+ 0
+ )
+
+ def assert_no_console_warnings(TestClass):
+ TestClass.assertEqual(
+ TestClass.driver.execute_script(
+ 'return window.tests.console.warn.length'
+ ),
+ 0
+ )
+
+ assert_no_console_warnings(TestClass)
+ assert_no_console_errors(TestClass)
diff --git a/tests/package.json b/tests/package.json
new file mode 100644
index 0000000..48a500d
--- /dev/null
+++ b/tests/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "dash_tests",
+ "version": "1.0.0",
+ "description": "Utilities to help with dash tests",
+ "main": "na",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "chris@plot.ly",
+ "license": "ISC"
+}
diff --git a/tests/test_react.py b/tests/test_react.py
new file mode 100644
index 0000000..830c723
--- /dev/null
+++ b/tests/test_react.py
@@ -0,0 +1,497 @@
+import unittest
+import json
+import pkgutil
+import plotly
+import dash_core_components as dcc
+from dash_html_components import Div
+import dash_renderer
+import dash
+
+from dash.dependencies import Event, Input, Output, State
+from dash import exceptions
+
+
+def generate_css(css_links):
+ return '\n'.join([
+ ' '.format(l)
+ for l in css_links
+ ])
+
+
+def generate_js(js_links):
+ return '\n'.join([
+ ''.format(l)
+ for l in js_links
+ ])
+
+
+class IntegrationTest(unittest.TestCase):
+ def setUp(self):
+ self.app = dash.Dash('my-app')
+ self.app.layout = Div([
+ Div('Hello World', id='header', style={'color': 'red'}),
+ dcc.Input(id='id1', placeholder='Type a value'),
+ dcc.Input(id='id2', placeholder='Type a value')
+ ])
+
+ self.client = self.app.server.test_client()
+
+ self.maxDiff = 100*1000
+
+ @unittest.skip('')
+ def test_route_list(self):
+ urls = [rule.rule for rule in self.app.server.url_map.iter_rules()]
+
+ self.assertEqual(
+ sorted(urls),
+ sorted([
+ '/interceptor',
+ '/initialize',
+ '/dependencies',
+ '/',
+ '/component-suites/',
+ '/static/'
+ ])
+ )
+
+ @unittest.skip('')
+ def test_initialize_route(self):
+ response = self.client.get('/initialize')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ json.loads(response.data),
+ json.loads(
+ json.dumps(self.app.layout, cls=plotly.utils.PlotlyJSONEncoder)
+ )
+ )
+
+ @unittest.skip('')
+ def test_dependencies_route(self):
+ self.app.callback('header', ['id1'])
+ response = self.client.get('/dependencies')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ json.loads(response.data), {
+ 'header': {
+ 'state': [{'id': 'id1'}],
+ 'events': [{'id': 'id1'}]
+ }
+ }
+ )
+
+ self.app.callback(
+ 'header',
+ state=[{'id': 'id1'}],
+ events=[{'id': 'id1'}])
+ response = self.client.get('/dependencies')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ json.loads(response.data), {
+ 'header': {
+ 'state': [{'id': 'id1'}],
+ 'events': [{'id': 'id1'}]
+ }
+ }
+ )
+
+ state = [
+ {'id': 'id1', 'prop': 'value'},
+
+ # Multiple properties from a single component
+ {'id': 'id1', 'prop': 'className'},
+
+ # Nested state
+ {'id': 'id1', 'prop': ['style', 'color']}
+ ]
+ events = [
+ {'id': 'id1', 'event': 'click'},
+ {'id': 'id1', 'event': 'submit'}
+ ]
+ self.app.callback('header', state=state, events=events)
+ response = self.client.get('/dependencies')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ json.loads(response.data), {
+ 'header': {
+ 'state': state,
+ 'events': events
+ }
+ }
+ )
+
+ @unittest.skip('')
+ def test_index_html(self):
+ response = self.client.get('/')
+ self.assertEqual(response.status_code, 200)
+
+ @unittest.skip('')
+ def test_single_observer_returning_a_dict(self):
+ @self.app.callback('header', ['id1'])
+ def update_header(input1):
+ self.assertEqual({'value': 'New Value'}, input1)
+ new_value = input1['value']
+ return {
+ 'children': new_value,
+ 'style.color': 'red',
+ 'className': 'active',
+ 'width': None
+ }
+
+ response = self.client.post(
+ '/interceptor',
+ headers={
+ 'Content-Type': 'application/json'
+ },
+ data=json.dumps({
+ # TODO - Why not just `target: id`?
+ 'target': {
+ 'props': {
+ 'id': 'header'
+ }
+ },
+ 'parents': {
+ 'id1': {
+ 'props': {
+ 'value': 'New Value'
+ }
+ }
+ }
+ })
+ )
+
+ self.assertEqual(
+ json.loads(response.data),
+ {
+ 'response': {
+ 'children': 'New Value',
+ 'props': {
+ 'id': 'header',
+ 'style.color': 'red',
+ 'className': 'active',
+ 'width': None
+ }
+ }
+ }
+ )
+
+ @unittest.skip('')
+ def test_single_observer_returning_a_component(self):
+ @self.app.callback('header', ['id1'])
+ def update_header(input1):
+ self.assertEqual({'value': 'New Value'}, input1)
+ return {
+ 'children': Div('New Component')
+ }
+
+ response = self.client.post(
+ '/interceptor',
+ headers={
+ 'Content-Type': 'application/json'
+ },
+ data=json.dumps({
+ 'target': {
+ 'props': {
+ 'id': 'header'
+ }
+ },
+ 'parents': {
+ 'id1': {
+ 'props': {
+ 'value': 'New Value'
+ }
+ }
+ }
+ })
+ )
+
+ self.assertEqual(
+ json.loads(response.data),
+ {
+ 'response': {
+ 'props': {
+ 'id': 'header'
+ },
+ 'children': {
+ 'type': 'Div',
+ 'namespace': 'html_components',
+ 'props': {
+ 'children': 'New Component'
+ }
+ }
+ }
+ }
+ )
+
+ @unittest.skip('')
+ def test_single_observer_updating_component_that_doesnt_exist(self):
+ # It's possible to register callbacks for components that don't
+ # exist in the initial layout because users could add them as
+ # children in response to another callback
+ @self.app.callback('doesnt-exist-yet', ['id1'])
+ def update_header(input1):
+ self.assertEqual({
+ 'value': 'New Value'
+ }, input1)
+
+ new_value = input1['value']
+ return {
+ 'value': new_value
+ }
+
+ response = self.client.post(
+ '/interceptor',
+ headers={
+ 'Content-Type': 'application/json'
+ },
+ data=json.dumps({
+ 'target': {
+ 'props': {
+ 'id': 'header'
+ }
+ },
+ 'parents': {
+ 'id1': {
+ 'props': {
+ 'value': 'New Value'
+ }
+ }
+ }
+ })
+ )
+
+ self.assertEqual(
+ json.loads(response.data),
+ {
+ 'response': {
+ 'props': {
+ 'id': 'doesnt-exit-yet',
+ 'value': 'New Value'
+ },
+ }
+ }
+ )
+
+ @unittest.skip('')
+ def test_single_observer_with_multiple_controllers(self):
+ @self.app.callback('header', ['id1', 'id2'])
+ def update_header(input1, input2):
+ self.assertEqual({
+ 'value': 'New Value'
+ }, input1)
+ self.assertEqual({}, input2)
+
+ new_value = input1['value']
+ return {
+ 'value': new_value
+ }
+
+ response = self.client.post(
+ '/interceptor',
+ headers={
+ 'Content-Type': 'application/json'
+ },
+ data=json.dumps({
+ 'target': {
+ 'props': {
+ 'id': 'header'
+ }
+ },
+ 'parents': {
+ 'id1': {
+ 'props': {
+ 'value': 'New Value'
+ }
+ },
+ 'id2': {
+ 'props': {}
+ }
+ }
+ })
+ )
+
+ self.assertEqual(
+ json.loads(response.data),
+ {
+ 'response': {
+ 'props': {
+ 'id': 'header',
+ 'value': 'New Value'
+ },
+ }
+ }
+ )
+
+ def test_serving_scripts(self):
+ self.app.scripts.config.serve_locally = True
+ self.app._setup_server()
+ response = self.client.get(
+ ('/_dash-component-suites/'
+ 'dash_renderer/bundle.js?v={}').format(dash_renderer.__version__)
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.data,
+ pkgutil.get_data('dash_renderer', 'bundle.js')
+ )
+
+
+class TestCallbacks(unittest.TestCase):
+ def test_callback_registry(self):
+ app = dash.Dash('')
+ input = dcc.Input(id='input')
+ input._events = ['blur', 'change']
+
+ app.layout = Div([
+ input,
+ Div(id='output-1'),
+ Div(id='output-2'),
+ Div(id='output-3')
+ ], id='body')
+
+ app.callback(
+ Output('body', 'children'),
+ [Input('input', 'value')]
+ )
+ app.callback(
+ Output('output-1', 'children'),
+ [Input('input', 'value')]
+ )
+ app.callback(
+ Output('output-2', 'children'),
+ [Input('input', 'value')],
+ state=[State('input', 'value')],
+ )
+ app.callback(
+ Output('output-3', 'children'),
+ [Input('input', 'value')],
+ state=[State('input', 'value')],
+ events=[Event('input', 'blur')],
+ )
+
+ def test_no_layout_exception(self):
+ app = dash.Dash('')
+ self.assertRaises(
+ exceptions.LayoutIsNotDefined,
+ app.callback,
+ Output('body', 'children'),
+ [Input('input', 'value')]
+ )
+
+ def test_exception_id_not_in_layout(self):
+ app = dash.Dash('')
+ app.layout = Div('', id='test')
+ self.assertRaises(
+ exceptions.NonExistantIdException,
+ app.callback,
+ Output('output', 'children'),
+ [Input('input', 'value')]
+ )
+
+ def test_exception_prop_not_in_component(self):
+ app = dash.Dash('')
+ app.layout = Div([
+ dcc.Input(id='input'),
+ Div(id='output')
+ ], id='body')
+
+ self.assertRaises(
+ exceptions.NonExistantPropException,
+ app.callback,
+ Output('output', 'non-there'),
+ [Input('input', 'value')]
+ )
+
+ self.assertRaises(
+ exceptions.NonExistantPropException,
+ app.callback,
+ Output('output', 'children'),
+ [Input('input', 'valuez')]
+ )
+
+ self.assertRaises(
+ exceptions.NonExistantPropException,
+ app.callback,
+ Output('body', 'childrenz'),
+ [Input('input', 'value')]
+ )
+
+ def test_exception_event_not_in_component(self):
+ app = dash.Dash('')
+ app.layout = Div([
+ Div(id='button'),
+ Div(id='output'),
+ Div(id='graph-output'),
+ dcc.Graph(id='graph')
+ ], id='body')
+
+ for id in ['output', 'body']:
+ self.assertRaises(
+ exceptions.NonExistantEventException,
+ app.callback,
+ Output(id, 'children'),
+ events=[Event(id, 'style')]
+ )
+ app.callback(
+ Output(id, 'children'),
+ events=[Event(id, 'click')]
+ )
+
+ self.assertRaises(
+ exceptions.NonExistantEventException,
+ app.callback,
+ Output('output', 'children'),
+ events=[Event('graph', 'zoom')]
+ )
+ app.callback(
+ Output('graph-output', 'children'),
+ events=[Event('graph', 'click')]
+ )
+
+ def test_exception_component_is_not_right_type(self):
+ app = dash.Dash('')
+ app.layout = Div([
+ dcc.Input(id='input'),
+ Div(id='output')
+ ], id='body')
+
+ test_args = [
+ ['asdf', ['asdf'], [], []],
+ [Output('output', 'children'), Input('input', 'value'), [], []],
+ [Output('output', 'children'), [], State('input', 'value'), []],
+ [Output('output', 'children'), [], [], Event('input', 'click')],
+ ]
+ for args in test_args:
+ self.assertRaises(
+ exceptions.IncorrectTypeException,
+ app.callback,
+ *args
+ )
+
+ def test_suppress_callback_exception(self):
+ app = dash.Dash('')
+ app.layout = Div([
+ dcc.Input(id='input'),
+ Div(id='output')
+ ], id='body')
+ self.assertRaises(
+ exceptions.NonExistantIdException,
+ app.callback,
+ Output('id-not-there', 'children'),
+ [Input('input', 'value')]
+ )
+ app.config.supress_callback_exceptions = True
+ app.callback(Output('id-not-there', 'children'),
+ [Input('input', 'value')])
+
+ def test_missing_input_and_events(self):
+ app = dash.Dash('')
+ app.layout = Div([
+ dcc.Input(id='input')
+ ], id='body')
+ self.assertRaises(
+ exceptions.MissingEventsException,
+ app.callback,
+ Output('body', 'children'),
+ [],
+ [State('input', 'value')]
+ )
diff --git a/tests/test_resources.py b/tests/test_resources.py
new file mode 100644
index 0000000..a25553a
--- /dev/null
+++ b/tests/test_resources.py
@@ -0,0 +1,164 @@
+import unittest
+import warnings
+from dash.resources import Scripts, Css
+from dash.development.base_component import generate_class
+
+
+def generate_components():
+ Div = generate_class('Div', ('children', 'id',), 'dash_html_components')
+ Span = generate_class('Span', ('children', 'id',), 'dash_html_components')
+ Input = generate_class(
+ 'Input', ('children', 'id',),
+ 'dash_core_components')
+ return Div, Span, Input
+
+
+def external_url(package_name):
+ return (
+ '//unpkg.com/{}@0.2.9'
+ '/{}/bundle.js'.format(
+ package_name.replace('_', '-'),
+ package_name
+ )
+ )
+
+
+def rel_path(package_name):
+ return '{}/bundle.js'.format(package_name)
+
+
+def abs_path(package_name):
+ return '/Users/chriddyp/{}/bundle.js'.format(package_name)
+
+
+class TestResources(unittest.TestCase):
+
+ def resource_test(self, css_or_js):
+ Div, Span, Input = generate_components()
+
+ if css_or_js == 'css':
+ # The CSS URLs and paths will look a little bit differently
+ # than the JS urls but that doesn't matter for the purposes
+ # of the test
+ Div._css_dist = Span._css_dist = [{
+ 'external_url': external_url('dash_html_components'),
+ 'relative_package_path': rel_path('dash_html_components')
+ }]
+
+ Input._css_dist = [{
+ 'external_url': external_url('dash_core_components'),
+ 'relative_package_path': rel_path('dash_core_components')
+ }]
+
+ else:
+ Div._js_dist = Span._js_dist = [{
+ 'external_url': external_url('dash_html_components'),
+ 'relative_package_path': rel_path('dash_html_components')
+ }]
+
+ Input._js_dist = [{
+ 'external_url': external_url('dash_core_components'),
+ 'relative_package_path': rel_path('dash_core_components')
+ }]
+
+ layout = Div([None, 'string', Span(), Div(Input())])
+
+ if css_or_js == 'css':
+ resources = Css(layout)
+ else:
+ resources = Scripts(layout)
+
+ resources._update_layout(layout)
+
+ expected_filtered_external_resources = [
+ {
+ 'external_url': external_url('dash_html_components'),
+ 'namespace': 'dash_html_components'
+ },
+ {
+ 'external_url': external_url('dash_core_components'),
+ 'namespace': 'dash_core_components'
+ }
+ ]
+ expected_filtered_relative_resources = [
+ {
+ 'relative_package_path': rel_path('dash_html_components'),
+ 'namespace': 'dash_html_components'
+ },
+ {
+ 'relative_package_path': rel_path('dash_core_components'),
+ 'namespace': 'dash_core_components'
+ }
+ ]
+
+ if css_or_js == 'css':
+ self.assertEqual(
+ resources.get_all_css(),
+ expected_filtered_external_resources
+ )
+ else:
+ self.assertEqual(
+ resources.get_all_scripts(),
+ expected_filtered_external_resources
+ )
+
+ resources.config.serve_locally = True
+ if css_or_js == 'css':
+ self.assertEqual(
+ resources.get_all_css(),
+ expected_filtered_relative_resources
+ )
+ else:
+ self.assertEqual(
+ resources.get_all_scripts(),
+ expected_filtered_relative_resources
+ )
+
+ resources.config.serve_locally = False
+ extra_resource = {'external_url': '//cdn.bootstrap.com/min.css'}
+ expected_resources = expected_filtered_external_resources + [
+ extra_resource
+ ]
+ if css_or_js == 'css':
+ resources.append_css(extra_resource)
+ self.assertEqual(
+ resources.get_all_css(),
+ expected_resources
+ )
+ else:
+ resources.append_script(extra_resource)
+ self.assertEqual(
+ resources.get_all_scripts(),
+ expected_resources
+ )
+
+ resources.config.serve_locally = True
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter('always')
+ if css_or_js == 'css':
+ self.assertEqual(
+ resources.get_all_css(),
+ expected_filtered_relative_resources
+ )
+ assert len(w) == 1
+ assert 'A local version of {} is not available'.format(
+ extra_resource['external_url']
+ ) in str(w[-1].message)
+
+ else:
+ self.assertEqual(
+ resources.get_all_scripts(),
+ expected_filtered_relative_resources
+ )
+ assert len(w) == 1
+ assert 'A local version of {} is not available'.format(
+ extra_resource['external_url']
+ ) in str(w[-1].message)
+
+ def test_js_resources(self):
+ # self.resource_test('js')
+ pass
+
+ def test_css_resources(self):
+ # self.resource_test('css')
+ pass
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..dba08c7
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,137 @@
+[tox]
+envlist = py{27,36,37}, py{27,36,37}dj{109,110,111,200,201}
+
+[testenv]
+deps =
+ dash_renderer
+ dash_flow_example==0.0.3
+ dash-dangerously-set-inner-html
+ selenium
+ tox
+ plotly>=2.0.8
+ pytest-django
+ flake8
+ pylint
+
+ dj109: Django>=1.9,<1.10
+ dj110: Django>=1.10,<1.11
+ dj111: Django>=1.11,<2.0
+ dj200: Django>=2.0,<2.1
+ dj201: Django>=2.1,<2.2
+passenv = *
+
+[py__]
+commands =
+ pip install "dash_core_components>=0.4.0" --no-deps
+ pip install "dash_html_components>=0.11.0" --no-deps
+ python --version
+ python -m unittest tests.development.test_base_component
+ python -m unittest tests.development.test_component_loader
+ python -m unittest tests.test_resources
+ flake8 dash setup.py
+
+[py__dj]
+changedir=tests/django_project
+commands =
+ pip install "dash_core_components>=0.4.0" --no-deps
+ pip install "dash_html_components>=0.11.0" --no-deps
+ python --version
+ python manage.py collectstatic --noinput
+ pytest
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+[testenv:py27]
+basepython={env:TOX_PYTHON_27}
+commands =
+ {[py__]commands}
+ pylint dash setup.py --rcfile={env:PYLINTRC}
+
+[testenv:py36]
+basepython={env:TOX_PYTHON_36}
+commands =
+ {[py__]commands}
+ pylint dash setup.py --rcfile={env:PYLINTRC}
+
+[testenv:py37]
+basepython={env:TOX_PYTHON_37}
+commands =
+ {[py__]commands}
+ pylint dash setup.py --rcfile={env:PYLINTRC37}
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+[testenv:py27dj109]
+basepython={[testenv:py27]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py27dj110]
+basepython={[testenv:py27]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py27dj111]
+basepython={[testenv:py27]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py27dj200]
+basepython={[testenv:py27]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py27dj201]
+basepython={[testenv:py27]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+[testenv:py36dj109]
+basepython={[testenv:py36]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py36dj110]
+basepython={[testenv:py36]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py36dj111]
+basepython={[testenv:py36]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py36dj200]
+basepython={[testenv:py36]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py36dj201]
+basepython={[testenv:py36]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+[testenv:py37dj109]
+basepython={[testenv:py37]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py37dj110]
+basepython={[testenv:py37]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py37dj111]
+basepython={[testenv:py37]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py37dj200]
+basepython={[testenv:py37]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}
+
+[testenv:py37dj201]
+basepython={[testenv:py37]basepython}
+changedir={[py__dj]changedir}
+commands={[py__dj]commands}