Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

twill

Package Overview
Dependencies
Maintainers
2
Versions
33
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

twill - npm Package Compare versions

Comparing version
3.2.5
to
3.3.1
extras/examples/flaskr-demo.twill

Sorry, the diff of this file is not supported yet

+17
-1

@@ -7,2 +7,16 @@ .. _changelog:

3.3.1 (released 2025-09-07)
---------------------------
* The supported Python versions are now 3.8 to 3.14.
3.3 (released 2024-10-13)
-------------------------
* The supported Python versions are now 3.8 to 3.13.
* Saving and loading of cookies has been made more robust, and it now also
incorporates the domains and paths of the cookies.
* Twill now uses Flask_ instead of Quixote_ as test server for running its own
test suite.
* Twill now uses 'argparse' instead of the deprecated 'optparse' to parse
options and arguments passed on the command line.
3.2.5 (released 2024-06-28)

@@ -150,3 +164,3 @@ ---------------------------

this is set to 2, so refresh intervals of 2 or more seconds are ignored.
* Moved the examples and additional stuff into an 'extras' directory.
* Moved the examples and additional stuff into an 'extras' directory.
* The documentation in the 'docs' directory has been updated and is now

@@ -169,1 +183,3 @@ created with Sphinx_.

.. _tox: https://tox.readthedocs.io/
.. _Quixote: https://github.com/nascheme/quixote
.. _Flask: https://flask.palletsprojects.com/
+7
-12

@@ -10,10 +10,6 @@ """Configuration file for the Sphinx documentation builder.

import os
import sys
from pathlib import Path
sys.path.append(
os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"
)
)
sys.path.append(str(Path(__file__).resolve().parent.parent / "src"))

@@ -23,11 +19,10 @@ # -- Project information -----------------------------------------------------

def project_version():
def project_version() -> str:
"""Fetch version from pyproject.toml file."""
# this also works when the package is not installed
with open("../pyproject.toml") as toml_file:
with open("../pyproject.toml", encoding="utf-8") as toml_file:
for line in toml_file:
if line.startswith("version ="):
version = line.split("=")[1].strip().strip('"')
return version
raise Exception("Cannot determine project version")
return line.split("=")[1].strip().strip('"')
raise RuntimeError("Cannot determine project version")

@@ -37,3 +32,3 @@

author = "C. Titus Brown, Ben R. Taylor, Christoph Zwerschke et al"
copyright = "2024, " + author
copyright = "2025, " + author

@@ -40,0 +35,0 @@ # The full version, including alpha/beta/rc tags

@@ -14,3 +14,3 @@ .. _developer:

twill comes with several unit tests. They depend on `pytest`_ and
`Quixote`_. To run them, type 'pytest' in the top level directory.
`Flask`_. To run them, type 'pytest' in the top level directory.
To run an individual test, you can use the command

@@ -20,3 +20,3 @@ ``pytest tests/test_something.py``.

.. _pytest: https://pytest.org/
.. _Quixote: https://github.com/nascheme/quixote
.. _Flask: https://flask.palletsprojects.com/

@@ -34,3 +34,3 @@ Licensing

Newer versions 1.x, 2.x and 3.x are also Copyright (C) 2007-2024
Newer versions 1.x, 2.x and 3.x are also Copyright (C) 2007-2025
Ben R. Taylor, Adam V. Brandizzi, Christoph Zwerschke et al.

@@ -37,0 +37,0 @@

@@ -79,3 +79,2 @@ .. _other:

.. _PyXPCOM: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_bindings/PyXPCOM
.. _Quixote: http://www.mems-exchange.org/software/quixote/
.. _httpx: https://www.python-httpx.org/

@@ -92,2 +91,1 @@ .. _Selenium: http://www.seleniumhq.org/

.. _zope.testbrowser: https://pypi.python.org/pypi/zope.testbrowser

@@ -76,5 +76,6 @@ .. _testing:

import os
import twill
from quixote.server.simple_server import run as quixote_run
from .server import app # a Flask app used as test server
from .utils import test_dir # directory with test scripts
PORT=8090 # port to run the server on

@@ -84,25 +85,28 @@

"""Function to run the server"""
quixote_run(twill.tests.server.create_publisher, port=PORT)
app.run(host=HOST, port=PORT)
def test():
"""The unit test"""
test_dir = twill.tests.utils.testdir
"""The test function"""
test_dir = twill.tests.utils.test_dir
script = os.path.join(test_dir, 'test_unit_support.twill')
# create test_info object
# create a TestInfo object
test_info = twill.unit.TestInfo(script, run_server, PORT)
# run tests!
# run the tests!
twill.unit.run_test(test_info)
Here, I'm unit testing the Quixote application ``twill.tests.server``, which
is run by ``quixote_run`` (a.k.a. ``quixote.server.simple_server.run``) on
port ``PORT``, using the twill script ``test_unit_support.twill``. That
script contains this code::
Here, I'm unit testing the Flask_ application ``.server`` in the ``tests``
directory, which is run on the specified ``PORT``, using the twill script
``test_unit_support.twill``. That script contains this code::
# starting URL is provided to it by the unit test support framework.
# starting URL is provided to it by the unit test support framework.
go ./multisubmitform
code 200
go ./multisubmitform
code 200
fv 1 sub_a click
submit
find "used_sub_a"
A few things to note:

@@ -109,0 +113,0 @@

The MIT License, https://opensource.org/licenses/MIT
Copyright 2005-2024 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke
Copyright 2005-2025 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke

@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy

+13
-10

@@ -1,4 +0,4 @@

Metadata-Version: 2.1
Metadata-Version: 2.4
Name: twill
Version: 3.2.5
Version: 3.3.1
Summary: A web browsing and testing language

@@ -9,3 +9,3 @@ Author: C. Titus Brown, Ben R. Taylor, Christoph Zwerschke

Copyright 2005-2024 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke
Copyright 2005-2025 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke

@@ -51,2 +51,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy

Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Other Scripting Engines

@@ -58,8 +60,8 @@ Classifier: Topic :: Internet :: WWW/HTTP

License-File: LICENSE.txt
Requires-Dist: lxml<6,>=5.2
Requires-Dist: httpx<1,>=0.27.0
Requires-Dist: lxml<7,>=6
Requires-Dist: httpx<1,>=0.28.1
Requires-Dist: pyparsing<4,>=3.1
Provides-Extra: docs
Requires-Dist: sphinx<8,>=7.3; extra == "docs"
Requires-Dist: sphinx_rtd_theme<3,>=2; extra == "docs"
Requires-Dist: sphinx<9,>=8.2; extra == "docs"
Requires-Dist: sphinx_rtd_theme<4,>=3; extra == "docs"
Provides-Extra: tidy

@@ -71,3 +73,4 @@ Requires-Dist: pytidylib<0.4,>=0.3; extra == "tidy"

Requires-Dist: pytidylib<0.4,>=0.3; extra == "tests"
Requires-Dist: quixote<4,>=3.6; extra == "tests"
Requires-Dist: flask<4,>=3.0; extra == "tests"
Dynamic: license-file

@@ -79,3 +82,3 @@ twill: a simple scripting language for web browsing

The current version 3.2 supports Python 3.8 to 3.12.
The current version 3.2 supports Python 3.8 to 3.14.

@@ -86,3 +89,3 @@ Take a look at the [changelog](https://twill-tools.github.io/twill/changelog.html) to find a list of all changes and improvements made since version 2. For a brief overview of twill's history starting from its early days, see the [acknowledgements and history](https://twill-tools.github.io/twill/overview.html#acknowledgements-and-history) section.

Copyright (c) 2005-2024 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke et al.
Copyright (c) 2005-2025 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke et al.

@@ -89,0 +92,0 @@ Newer versions have been created and are maintained by [Christoph Zwerschke](https://github.com/Cito).

[project]
name = "twill"
version = "3.2.5"
version = "3.3.1"

@@ -35,2 +35,4 @@ description = "A web browsing and testing language"

'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
'Programming Language :: Other Scripting Engines',

@@ -43,4 +45,4 @@ 'Topic :: Internet :: WWW/HTTP',

dependencies = [
"lxml >=5.2, <6",
"httpx >=0.27.0, <1",
"lxml >=6, <7",
"httpx >=0.28.1, <1",
"pyparsing >=3.1, <4",

@@ -51,4 +53,4 @@ ]

docs = [
"sphinx >=7.3, <8",
"sphinx_rtd_theme >=2, <3"
"sphinx >=8.2, <9",
"sphinx_rtd_theme >=3, <4"
]

@@ -62,3 +64,3 @@ tidy = [

"pytidylib >=0.3, <0.4",
"quixote >=3.6, <4",
"flask >=3.0, <4",
]

@@ -86,3 +88,3 @@

[tool.mypy]
python_version = 3.12
python_version = 3.13
check_untyped_defs = true

@@ -161,3 +163,2 @@ no_implicit_optional = true

"ANN002", "ANN003", # no type annotations needed for args and kwargs
"ANN101", "ANN102", # no type annotation for self and cls needed
"ANN401", # allow explicit Any

@@ -169,2 +170,3 @@ "COM812", # allow trailing commas for auto-formatting

"ISC001", # allow string literal concatenation for auto-formatting
"PLW0603", # allow global statements
"PTH123", # allow builtin-open

@@ -171,0 +173,0 @@ "TRY003", # allow specific messages outside the exception class

@@ -6,3 +6,3 @@ twill: a simple scripting language for web browsing

The current version 3.2 supports Python 3.8 to 3.12.
The current version 3.2 supports Python 3.8 to 3.14.

@@ -13,3 +13,3 @@ Take a look at the [changelog](https://twill-tools.github.io/twill/changelog.html) to find a list of all changes and improvements made since version 2. For a brief overview of twill's history starting from its early days, see the [acknowledgements and history](https://twill-tools.github.io/twill/overview.html#acknowledgements-and-history) section.

Copyright (c) 2005-2024 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke et al.
Copyright (c) 2005-2025 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke et al.

@@ -16,0 +16,0 @@ Newer versions have been created and are maintained by [Christoph Zwerschke](https://github.com/Cito).

@@ -0,0 +0,0 @@ [egg_info]

@@ -1,4 +0,4 @@

Metadata-Version: 2.1
Metadata-Version: 2.4
Name: twill
Version: 3.2.5
Version: 3.3.1
Summary: A web browsing and testing language

@@ -9,3 +9,3 @@ Author: C. Titus Brown, Ben R. Taylor, Christoph Zwerschke

Copyright 2005-2024 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke
Copyright 2005-2025 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke

@@ -51,2 +51,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy

Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Other Scripting Engines

@@ -58,8 +60,8 @@ Classifier: Topic :: Internet :: WWW/HTTP

License-File: LICENSE.txt
Requires-Dist: lxml<6,>=5.2
Requires-Dist: httpx<1,>=0.27.0
Requires-Dist: lxml<7,>=6
Requires-Dist: httpx<1,>=0.28.1
Requires-Dist: pyparsing<4,>=3.1
Provides-Extra: docs
Requires-Dist: sphinx<8,>=7.3; extra == "docs"
Requires-Dist: sphinx_rtd_theme<3,>=2; extra == "docs"
Requires-Dist: sphinx<9,>=8.2; extra == "docs"
Requires-Dist: sphinx_rtd_theme<4,>=3; extra == "docs"
Provides-Extra: tidy

@@ -71,3 +73,4 @@ Requires-Dist: pytidylib<0.4,>=0.3; extra == "tidy"

Requires-Dist: pytidylib<0.4,>=0.3; extra == "tests"
Requires-Dist: quixote<4,>=3.6; extra == "tests"
Requires-Dist: flask<4,>=3.0; extra == "tests"
Dynamic: license-file

@@ -79,3 +82,3 @@ twill: a simple scripting language for web browsing

The current version 3.2 supports Python 3.8 to 3.12.
The current version 3.2 supports Python 3.8 to 3.14.

@@ -86,3 +89,3 @@ Take a look at the [changelog](https://twill-tools.github.io/twill/changelog.html) to find a list of all changes and improvements made since version 2. For a brief overview of twill's history starting from its early days, see the [acknowledgements and history](https://twill-tools.github.io/twill/overview.html#acknowledgements-and-history) section.

Copyright (c) 2005-2024 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke et al.
Copyright (c) 2005-2025 by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke et al.

@@ -89,0 +92,0 @@ Newer versions have been created and are maintained by [Christoph Zwerschke](https://github.com/Cito).

@@ -1,8 +0,8 @@

lxml<6,>=5.2
httpx<1,>=0.27.0
lxml<7,>=6
httpx<1,>=0.28.1
pyparsing<4,>=3.1
[docs]
sphinx<8,>=7.3
sphinx_rtd_theme<3,>=2
sphinx<9,>=8.2
sphinx_rtd_theme<4,>=3

@@ -13,5 +13,5 @@ [tests]

pytidylib<0.4,>=0.3
quixote<4,>=3.6
flask<4,>=3.0
[tidy]
pytidylib<0.4,>=0.3

@@ -24,2 +24,3 @@ LICENSE.txt

extras/examples/extend-example.py
extras/examples/flaskr-demo.twill
extras/examples/quixote-demo.twill

@@ -26,0 +27,0 @@ extras/examples/set-user-agent.twill

@@ -6,3 +6,3 @@ # This file is part of the twill source distribution.

#
# Copyright (c) 2005-2024
# Copyright (c) 2005-2025
# by C. Titus Brown, Ben R. Taylor, Christoph Zwerschke et al.

@@ -33,2 +33,5 @@ #

__all__ = [
"TwillCommandLoop",
"__url__",
"__version__",
"browser",

@@ -38,9 +41,6 @@ "execute_file",

"log",
"set_err_out",
"set_log_level",
"set_output",
"set_err_out",
"twill_ext",
"TwillCommandLoop",
"__url__",
"__version__",
]

@@ -89,3 +89,3 @@

"""
global handler # noqa: PLW0603
global handler
if stream is None:

@@ -92,0 +92,0 @@ stream = stdout

@@ -5,3 +5,2 @@ """Map of various User-Agent string shortcuts that can be used for testing."""

# noinspection HttpUrlsUsage
agents: Dict[str, str] = {

@@ -8,0 +7,0 @@ # Desktop

@@ -57,3 +57,3 @@ """Implementation of the TwillBrowser."""

"""Set the debug level for the connection pool."""
from http.client import HTTPConnection
from http.client import HTTPConnection # noqa: PLC0415

@@ -73,3 +73,3 @@ HTTPConnection.debuglevel = level

follow_redirects: bool = True, # noqa: FBT001, FBT002
verify: Union[bool, str] = False, # noqa: FBT002
verify: Union[bool, str] = False, # noqa: FBT001, FBT002
timeout: Union[None, float, Timeout] = 10,

@@ -131,3 +131,3 @@ ) -> None:

follow_redirects: bool = True, # noqa: FBT001, FBT002
verify: Union[bool, str] = False, # noqa: FBT002
verify: Union[bool, str] = False, # noqa: FBT001,FBT002
timeout: Union[None, float, Timeout] = 10,

@@ -205,3 +205,2 @@ ) -> None:

if not url.startswith((".", "/", "?")):
# noinspection HttpUrlsUsage
try_urls.append(f"http://{url}")

@@ -327,3 +326,3 @@ try_urls.append(f"https://{url}")

"""Set the request timeout in seconds."""
self._client.timeout = timeout # type: ignore[assignment]
self._client.timeout = timeout

@@ -415,3 +414,3 @@ def show_forms(self) -> None:

return CheckboxGroup(
cast(List[InputElement], match_name)
cast("List[InputElement]", match_name)
)

@@ -421,3 +420,5 @@ if all(

):
return RadioGroup(cast(List[InputElement], match_name))
return RadioGroup(
cast("List[InputElement]", match_name)
)
else:

@@ -482,3 +483,3 @@ match_name = None

if getattr(control, "type", None) in ("submit", "image"):
self.last_submit_button = cast(InputElement, control)
self.last_submit_button = cast("InputElement", control)

@@ -521,3 +522,3 @@ def submit(

if submits:
ctl = cast(InputElement, submits[0])
ctl = cast("InputElement", submits[0])
else:

@@ -527,3 +528,3 @@ ctl = self.last_submit_button

# field name given; find it
ctl = cast(InputElement, self.form_field(form, field_name))
ctl = cast("InputElement", self.form_field(form, field_name))

@@ -536,4 +537,3 @@ # now set up the submission by building the request object that

log.info(
"Note: submit is using submit button:"
" name='%s', value='%s'",
"Note: submit is using submit button: name='%s', value='%s'",
ctl.get("name"),

@@ -590,9 +590,17 @@ ctl.value,

"""Save cookies into the given file."""
saved_cookies = [
(cookie.name, cookie.value, cookie.domain, cookie.path)
for cookie in self._client.cookies.jar
]
with open(filename, "wb") as f:
pickle.dump(dict(self._client.cookies), f)
pickle.dump(saved_cookies, f)
def load_cookies(self, filename: str) -> None:
"""Load cookies from the given file."""
cookies = Cookies()
with open(filename, "rb") as f:
self._client.cookies = pickle.load(f) # noqa: S301
loaded_cookies = pickle.load(f) # noqa: S301
for name, value, domain, path in loaded_cookies:
cookies.set(name, value, domain=domain, path=path)
self._client.cookies = cookies

@@ -614,3 +622,3 @@ def clear_cookies(self) -> None:

else:
log.info("\nThere are no cookies in the cookie jar.\n", n)
log.info("\nThere are no cookies in the cookie jar.\n")

@@ -736,3 +744,3 @@ def decode(self, value: Union[bytes, str]) -> str:

break
if interval >= refresh_interval:
if interval and interval >= refresh_interval:
(log.info if self.show_refresh else log.debug)(

@@ -751,5 +759,7 @@ "Meta refresh interval too long: %d", interval

if func_name in ("follow_link", "open") and (
if (
func_name in ("follow_link", "open")
# if we're really reloading and just didn't say so, don't store
self.result is not None and self.result.url != result.url
and self.result is not None
and self.result.url != result.url
):

@@ -756,0 +766,0 @@ self._history.append(self.result)

@@ -20,3 +20,2 @@ """The twill shell commands.

# noinspection SpellCheckingInspection
__all__ = [

@@ -37,17 +36,17 @@ "add_auth",

"extend_with",
"fa",
"find",
"follow",
"form_action",
"form_clear",
"form_file",
"form_value",
"formaction",
"fa",
"form_clear",
"formclear",
"form_file",
"formfile",
"form_value",
"formvalue",
"fv",
"get_input",
"get_password",
"getinput",
"get_password",
"getpassword",

@@ -66,24 +65,24 @@ "go",

"reset_output",
"rf",
"run",
"run_file",
"runfile",
"rf",
"save_cookies",
"save_html",
"set_global",
"set_local",
"setglobal",
"set_global",
"setlocal",
"set_local",
"show",
"showcookies",
"show_cookies",
"show_extra_headers",
"show_forms",
"show_history",
"show_html",
"show_links",
"showcookies",
"showforms",
"show_forms",
"showhistory",
"show_history",
"showhtml",
"show_html",
"showlinks",
"show_links",
"sleep",

@@ -108,3 +107,2 @@ "submit",

# noinspection PyShadowingBuiltins
def exit(code: str = "0") -> None:

@@ -193,4 +191,3 @@ """>> exit [<code>]

raise TwillAssertionError(
f"current url is '{current_url}';\n"
f"does not match '{should_be}'\n"
f"current url is '{current_url}';\ndoes not match '{should_be}'\n"
)

@@ -281,3 +278,2 @@

# noinspection SpellCheckingInspection
notfind = not_find # backward compatibility and convenience

@@ -325,3 +321,2 @@

# noinspection SpellCheckingInspection
showhtml = show_html # backward compatibility and consistency

@@ -424,3 +419,2 @@

# noinspection SpellCheckingInspection
showforms = show_forms # backward compatibility and convenience

@@ -439,3 +433,2 @@

# noinspection SpellCheckingInspection
showlinks = show_links # backward compatibility and convenience

@@ -454,3 +447,2 @@

# noinspection SpellCheckingInspection
showhistory = show_history # backward compatibility and convenience

@@ -477,3 +469,2 @@

# noinspection SpellCheckingInspection
formclear = form_clear # backward compatibility and convenience

@@ -533,3 +524,2 @@

# noinspection SpellCheckingInspection
fv = formvalue = form_value # backward compatibility and convenience

@@ -552,3 +542,2 @@

# noinspection SpellCheckingInspection
fa = formaction = form_action # backward compatibility and convenience

@@ -587,3 +576,2 @@

# noinspection SpellCheckingInspection
formfile = form_file # backward compatibility and convenience

@@ -606,3 +594,3 @@

from . import parse, shell
from . import parse, shell # noqa: PLC0415

@@ -645,3 +633,2 @@ fn_list = getattr(mod, "__all__", None)

# noinspection SpellCheckingInspection
getinput = get_input # backward compatibility and convenience

@@ -665,3 +652,2 @@

# noinspection SpellCheckingInspection
getpassword = get_password # backward compatibility and convenience

@@ -704,3 +690,2 @@

# noinspection SpellCheckingInspection
showcookies = show_cookies # backward compatibility and convenience

@@ -736,3 +721,3 @@

"""
from . import parse
from . import parse # noqa: PLC0415

@@ -780,3 +765,3 @@ try:

"""
from . import parse
from . import parse # noqa: PLC0415

@@ -788,3 +773,2 @@ filenames = utils.gather_filenames(args)

# noinspection SpellCheckingInspection
rf = runfile = run_file # backward compatibility and convenience

@@ -814,3 +798,2 @@

# noinspection SpellCheckingInspection
setglobal = set_global # backward compatibility and convenience

@@ -828,3 +811,2 @@

# noinspection SpellCheckingInspection
setlocal = set_local # backward compatibility and convenience

@@ -831,0 +813,0 @@

@@ -20,3 +20,3 @@ """Extension functions for parsing sys.argv.

"""
global_dict, local_dict = namespaces.get_twill_glocals()
global_dict, _local_dict = namespaces.get_twill_glocals()

@@ -26,3 +26,3 @@ require = int(require)

if len(shell.twill_args) < require:
from twill.errors import TwillAssertionError
from twill.errors import TwillAssertionError # noqa: PLC0415

@@ -29,0 +29,0 @@ given = len(shell.twill_args)

@@ -24,3 +24,3 @@ """Extension functions to check all of the links on a page.

__all__ = ["check_links", "report_bad_links", "good_urls", "bad_urls"]
__all__ = ["bad_urls", "check_links", "good_urls", "report_bad_links"]

@@ -71,3 +71,2 @@ # first, set up config options & persistent 'bad links' memory...

# noinspection HttpUrlsUsage
if not url.startswith(("http://", "https://")):

@@ -74,0 +73,0 @@ debug("url '%s' is not an HTTP link; ignoring", url)

@@ -23,3 +23,3 @@ """Extension functions for easier form filling.

__all__ = ["fv_match", "fv_multi_match", "fv_multi", "fv_multi_sub"]
__all__ = ["fv_match", "fv_multi", "fv_multi_match", "fv_multi_sub"]

@@ -26,0 +26,0 @@

@@ -78,3 +78,3 @@ """Suresh's extension for slicing and dicing using regular expressions."""

"""Evaluate an expression."""
return eval(exp, globals(), {"m": match}) # noqa: PGH001, S307
return eval(exp, globals(), {"m": match}) # noqa: S307

@@ -81,0 +81,0 @@

@@ -18,3 +18,3 @@ """A simple set of extensions to manage post-load requirements for pages.

__all__ = ["require", "skip_require", "flush_visited", "no_require"]
__all__ = ["flush_visited", "no_require", "require", "skip_require"]

@@ -47,3 +47,2 @@ _requirements = [] # what requirements to satisfy

# install the post-load hook function.
# noinspection PyProtectedMember
hooks = browser._post_load_hooks # noqa: SLF001

@@ -65,3 +64,2 @@ if _require_post_load_hook not in hooks:

"""
# noinspection PyProtectedMember
hooks = browser._post_load_hooks # noqa: SLF001

@@ -79,3 +77,3 @@ hooks = [fn for fn in hooks if fn != _require_post_load_hook]

"""
from .check_links import good_urls
from .check_links import good_urls # noqa: PLC0415

@@ -103,3 +101,3 @@ good_urls.clear()

elif what == "links_ok":
from .check_links import check_links, good_urls
from .check_links import check_links, good_urls # noqa: PLC0415

@@ -106,0 +104,0 @@ Ignore.always = True

@@ -5,3 +5,3 @@ """Used in test_shell, to test default command execution and extensions."""

__all__ = ["set_flag", "unset_flag", "assert_flag_set", "assert_flag_unset"]
__all__ = ["assert_flag_set", "assert_flag_unset", "set_flag", "unset_flag"]

@@ -8,0 +8,0 @@

@@ -6,6 +6,6 @@ """The twill multiprocess execution system."""

import time
from optparse import OptionParser
from argparse import ArgumentParser
from pickle import dump, load
from . import execute_file, set_log_level
from twill import execute_file, set_log_level

@@ -22,8 +22,7 @@

parser = OptionParser()
add = parser.add_option
parser = ArgumentParser()
add = parser.add_argument
add(
"-u",
"--url",
nargs=1,
action="store",

@@ -36,7 +35,6 @@ dest="url",

"--number",
nargs=1,
action="store",
dest="number",
default=1,
type="int",
type=int,
help="number of times to run the given script(s)",

@@ -47,15 +45,17 @@ )

"--processes",
nargs=1,
action="store",
dest="processes",
default=1,
type="int",
type=int,
help="number of processes to execute in parallel",
)
add(
"scripts",
metavar="SCRIPT",
nargs="+",
help="one or more twill scripts to execute",
)
options, args = parser.parse_args()
args = parser.parse_args()
if not args:
sys.exit("Error: Must specify one or more scripts to execute.")
# make sure that the current working directory is in the path

@@ -65,4 +65,4 @@ if "" not in sys.path:

average_number = options.number // options.processes
last_number = average_number + options.number % options.processes
average_number = args.number // args.processes
last_number = average_number + args.number % args.processes
child_pids = []

@@ -73,3 +73,3 @@ is_parent = True

# start a bunch of child processes and record their pids in the parent
for i in range(options.processes):
for i in range(args.processes):
pid = fork()

@@ -106,3 +106,3 @@ if pid:

else: # record statistics, otherwise
filename = ".status.%d" % (child_pid,)
filename = f".status.{child_pid}"
with open(filename, "rb") as fp:

@@ -116,3 +116,3 @@ this_time, n_executed = load(fp) # noqa: S301

print("\n----\n")
print(f"number of processes: {options.processes}")
print(f"number of processes: {args.processes}")
print(f"total executed: {total_exec}")

@@ -135,4 +135,4 @@ print(f"total time to execute: {total_time:.2f} s")

for _i in range(repeat):
for filename in args:
execute_file(filename, initial_url=options.url)
for filename in args.scripts:
execute_file(filename, initial_url=args.url)

@@ -139,0 +139,0 @@ end_time = time.time()

@@ -16,4 +16,3 @@ """Global and local dictionaries, and initialization/utility functions."""

"""
# noinspection PyCompatibility
from . import commands, parse
from . import commands, parse # noqa: PLC0415

@@ -20,0 +19,0 @@ cmd_list = commands.__all__

@@ -24,3 +24,2 @@ """Code parsing and evaluation for the twill mini-language."""

# noinspection PyCompatibility
from . import commands, log, namespaces

@@ -212,3 +211,3 @@ from .browser import browser

with (
nullcontext(sys.stdin) # type: ignore[attr-defined]
nullcontext(sys.stdin)
if filename == "-"

@@ -268,4 +267,3 @@ else open(filename, encoding="utf-8")

error_context = (
f"{error_type} raised on line {line_no}"
f" of '{source_info}'"
f"{error_type} raised on line {line_no} of '{source_info}'"
)

@@ -304,3 +302,3 @@ if line:

"""Turn printing of commands as they are executed on or off."""
global _log_commands # noqa: PLW0603
global _log_commands
old_flag = _log_commands is log.info

@@ -307,0 +305,0 @@ _log_commands = log.info if flag else log.debug

@@ -10,10 +10,9 @@ """A command-line interpreter for twill.

import traceback
from argparse import ArgumentParser
from cmd import Cmd
from contextlib import suppress
from io import TextIOWrapper
from optparse import OptionParser
from pathlib import Path
from typing import Any, Callable, List, Optional
from textwrap import dedent
from typing import IO, Any, Callable, List, Optional
# noinspection PyCompatibility
from . import (

@@ -96,2 +95,9 @@ __url__,

message = message.strip()
try:
title, details = message.split("\n", 1)
except ValueError:
pass
else: # dedent the details (for Python < 3.13)
details = dedent(details)
message = "\n".join((title, details))
max_width = max(

@@ -130,3 +136,3 @@ 7 + len(cmd),

self,
stdin: Optional[TextIOWrapper] = None,
stdin: Optional[IO[str]] = None,
initial_url: Optional[str] = None,

@@ -159,3 +165,3 @@ *,

global_dict, local_dict = namespaces.get_twill_glocals()
global_dict, _local_dict = namespaces.get_twill_glocals()

@@ -193,9 +199,10 @@ # add all of the commands from twill

"""
cmd, args = parse.parse_command(line + ".", {}, {})
place = len(args)
if place == 1:
return self.provide_form_name(text)
if place == 2: # noqa: PLR2004
form_name = args[0]
return self.provide_field_name(form_name, text)
_cmd, args = parse.parse_command(line + ".", {}, {})
if args:
place = len(args) if args else 0
if place == 1:
return self.provide_form_name(text)
if place == 2: # noqa: PLR2004
form_name = args[0]
return self.provide_field_name(form_name, text)
return []

@@ -268,3 +275,3 @@

parse.execute_command(
cmd, args, global_dict, local_dict, "<shell>"
cmd, args or (), global_dict, local_dict, "<shell>"
)

@@ -330,3 +337,3 @@ except SystemExit:

"""Run as shell script."""
global interactive # noqa: PLW0603
global interactive

@@ -343,4 +350,4 @@ # show the shorthand name for usage

parser = OptionParser()
add = parser.add_option
parser = ArgumentParser()
add = parser.add_argument

@@ -371,3 +378,2 @@ add(

"--loglevel",
nargs=1,
action="store",

@@ -387,3 +393,2 @@ dest="loglevel",

"--output",
nargs=1,
action="store",

@@ -403,3 +408,2 @@ dest="outfile",

"--url",
nargs=1,
action="store",

@@ -423,24 +427,31 @@ dest="url",

)
add(
"scripts",
metavar="SCRIPT",
nargs="*",
help="the twill script to execute",
)
# parse arguments
args = argv[1:]
if "--" in args:
for last in range(len(args) - 1, -1, -1):
if args[last] == "--":
twill_args[:] = args[last + 1 :]
args = args[:last]
raw_args = argv[1:]
if "--" in raw_args:
for last in reversed(range(len(raw_args))):
if raw_args[last] == "--":
twill_args[:] = raw_args[last + 1 :]
raw_args = raw_args[:last]
break
options, args = parser.parse_args(args)
args = parser.parse_args(raw_args)
if options.show_version:
if args.show_version:
log.info(version_info)
sys.exit(0)
quiet = options.quiet
show_browser = options.show_browser
dump_file = options.dumpfile
out_file = options.outfile
log_level = options.loglevel
interactive = options.interactive or not args
quiet = args.quiet
show_browser = args.show_browser
dump_file = args.dumpfile
out_file = args.outfile
log_level = args.loglevel
scripts = args.scripts
interactive = args.interactive or not scripts

@@ -455,3 +466,3 @@ if out_file:

if options.show_browser and (not dump_file or dump_file == "-"):
if show_browser and (not dump_file or dump_file == "-"):
sys.exit("Please also specify a dump file with -d")

@@ -461,3 +472,3 @@

log_level = log_level.lstrip("=").lstrip() or None
if log_level.upper() not in log_levels:
if log_level and log_level.upper() not in log_levels:
log_level_names = ", ".join(sorted(log_levels))

@@ -482,7 +493,7 @@ sys.exit(f"Valid log levels are: {log_level_names}")

failed = False
if args:
if scripts:
success = []
failure = []
filenames = gather_filenames(args)
filenames = gather_filenames(scripts)
dump = None

@@ -495,4 +506,4 @@

filename,
initial_url=options.url,
never_fail=options.never_fail,
initial_url=args.url,
never_fail=args.never_fail,
)

@@ -503,3 +514,3 @@ success.append(filename)

dump = browser.dump
if options.fail:
if args.fail:
raise

@@ -532,3 +543,3 @@ if browser.first_error:

)
if len(failure):
if failure:
log.error("Failed:\n\t%s", "\n\t".join(failure))

@@ -538,3 +549,3 @@ failed = True

if dump and show_browser:
import webbrowser
import webbrowser # noqa: PLC0415

@@ -548,5 +559,5 @@ url = Path(dump_file).absolute().as_uri()

if interactive:
welcome_msg = "" if args else "\n -= Welcome to twill =-\n"
welcome_msg = "" if scripts else "\n -= Welcome to twill =-\n"
shell = TwillCommandLoop(initial_url=options.url)
shell = TwillCommandLoop(initial_url=args.url)

@@ -559,4 +570,2 @@ while True:

break
except SystemExit:
raise

@@ -563,0 +572,0 @@ welcome_msg = ""

@@ -31,2 +31,3 @@ """Support functionality for using twill in unit tests."""

server_fn: Callable[[], None],
host: str = HOST,
port: int = PORT,

@@ -38,2 +39,3 @@ sleep: float = SLEEP,

self.server_fn = server_fn
self.host = host
self.port = port

@@ -68,4 +70,3 @@ self.stdout: Optional[TextIO] = None

"""Get the test server URL."""
# noinspection HttpUrlsUsage
return f"http://{HOST}:{self.port}/"
return f"http://{self.host}:{self.port}/"

@@ -72,0 +73,0 @@

@@ -38,2 +38,15 @@ """Various ugly utility functions for twill.

__all__ = [
"CheckboxGroup",
"FieldElement",
"FormElement",
"HtmlElement",
"InputElement",
"Link",
"RadioGroup",
"Response",
"ResultWrapper",
"SelectElement",
"Singleton",
"TextareaElement",
"UrlWithRealm",
"gather_filenames",

@@ -44,6 +57,6 @@ "get_equiv_refresh_interval",

"is_twill_filename",
"print_form",
"make_boolean",
"make_int",
"make_twill_filename",
"print_form",
"run_tidy",

@@ -53,15 +66,2 @@ "tree_to_html",

"unique_match",
"CheckboxGroup",
"FieldElement",
"FormElement",
"HtmlElement",
"InputElement",
"Link",
"RadioGroup",
"ResultWrapper",
"SelectElement",
"Singleton",
"TextareaElement",
"UrlWithRealm",
"Response",
]

@@ -435,3 +435,3 @@

"""
from .commands import options
from .commands import options # noqa: PLC0415

@@ -457,7 +457,7 @@ require_tidy = options.get("require_tidy")

def get_equiv_refresh_interval() -> Optional[int]:
"""Get the smallest interval for which the browser should follow redirects.
"""Get the longest interval for which the browser should follow redirects.
Redirection happens if the given interval is smaller than this.
Redirection happens if the given interval is shorter than this.
"""
from .commands import options
from .commands import options # noqa: PLC0415

@@ -464,0 +464,0 @@ return options.get("equiv_refresh_interval")

@@ -19,4 +19,4 @@ """Shared test configuration for pytest."""

from twill import set_output
from twill.commands import find, go
from twill import set_output # noqa: PLC0415
from twill.commands import find, go # noqa: PLC0415

@@ -44,6 +44,6 @@ set_output()

@pytest.fixture()
@pytest.fixture
def output() -> Generator[StringIO, None, None]:
"""Get output from the test."""
from twill import set_output
from twill import set_output # noqa: PLC0415

@@ -50,0 +50,0 @@ with StringIO() as output:

@@ -39,3 +39,3 @@ """Simple mock implementation of dnspython to test the twill DNS extension."""

sys.modules["dns"] = package
for module in "ipv4 name rdatatype resolver".split():
for module in ["ipv4", "name", "rdatatype", "resolver"]:
sys.modules[f"dns.{module}"] = package

@@ -42,0 +42,0 @@ setattr(package, module, package)

#!/usr/bin/env python3
"""Quixote test app for twill."""
"""Flask based test app for twill."""

@@ -8,531 +8,364 @@ import os

from time import sleep
from typing import Optional
from typing import Any, MutableMapping, NoReturn, Optional
from quixote import ( # type: ignore[import-untyped]
get_path,
get_request,
get_response,
get_session,
get_session_manager,
from flask import (
Flask,
Response,
abort,
make_response,
redirect,
request,
session,
)
from quixote.directory import ( # type: ignore[import-untyped]
AccessControlled,
Directory,
)
from quixote.errors import AccessError # type: ignore[import-untyped]
from quixote.form import widget # type: ignore[import-untyped]
from quixote.publish import Publisher # type: ignore[import-untyped]
from quixote.session import ( # type: ignore[import-untyped]
Session,
SessionManager,
)
HOST = "127.0.0.1"
PORT = 8080
DEBUG = False
RELOAD = False
app = Flask(__name__)
class AlwaysSession(Session):
"""Session that always saves."""
app.secret_key = "not-a-secret-since-this-is-a-test-app-only" # noqa: S105
def __init__(self, session_id: str) -> None:
"""Initialize the session."""
Session.__init__(self, session_id)
self.visit = 0
def has_info(self) -> bool:
"""Return true to indicate that it should always save."""
return True
class SessionManager:
"""Session manager."""
is_dirty = has_info
_next_session_id = 1
@classmethod
def message(cls, session: MutableMapping) -> str:
"""Create a message with session information."""
sid = cls.session_id(session) or "undefined"
visit = session.get("visit", 0)
user = session.get("user", "guest")
return f"""\
<html>
<head>
<title>Hello, world!</title>
</head>
<body>
<p>Hello, world!</p>
<p>These are the twill tests.</p>
<p>Your session ID is {sid}; this is visit #{visit}.</p>
<p>You are logged in as {user}.</p>
<p>
<a href="increment">increment</a> |
<a href="incrementfail">incrementfail</a>
</p>
<p><a href="logout">log out</a></p>
<p>
(<a href="test spaces">test spaces</a> /
<a href="test_spaces">test spaces2</a>)
</p>
</body>
</html>
"""
class UnauthorizedError(AccessError):
"""Error used for Basic Authentication.
@classmethod
def session_id(cls, session: MutableMapping) -> Optional[int]:
"""Get the session ID or set it if it does not exist."""
sid = session.get("sid")
if sid is None:
session["sid"] = cls._next_session_id
cls._next_session_id += 1
return sid
The request requires user authentication.
This subclass of AccessError sends a 401 instead of a 403,
hinting that the client should try again with authentication.
(from http://quixote.ca/qx/HttpBasicAuthentication)
"""
status_code = 401
title = "Unauthorized"
description = "You are not authorized to access this resource."
message = SessionManager.message
def __init__(
self,
realm: str = "Protected",
public_msg: Optional[str] = None,
private_msg: Optional[str] = None,
) -> None:
"""Initialize the error."""
self.realm = realm
AccessError.__init__(self, public_msg, private_msg)
def format(self) -> str:
"""Format the error."""
request = get_request()
request.response.set_header(
"WWW-Authenticate", f'Basic realm="{self.realm}"'
)
return AccessError.format(self)
# HTML helpers
def create_publisher() -> None:
"""Create a publisher for TwillTest, with session management added on."""
session_manager = SessionManager(session_class=AlwaysSession)
return Publisher(
TwillTest(),
session_manager=session_manager,
display_exceptions="plain",
)
def field(type_: str = "text", name: str = "", value: str = "") -> str:
"""Create an HTML field."""
return f'<input type="{type_}" name="{name}" value="{value}">'
def message(session: AlwaysSession) -> str:
"""Create a message with session information."""
return f"""\
<html>
<head>
<title>Hello, world!</title>
</head>
<body>
Hello, world!
<p>
These are the twill tests.
<p>
Your session ID is {session.id}; this is visit #{session.visit}.
<p>
You are logged in as "{session.user}".
<p>
<a href="./increment">increment</a> |
<a href="./incrementfail">incrementfail</a>
<p>
<a href="logout">log out</a>
<p>
(<a href="test spaces">test spaces</a> /
<a href="test_spaces">test spaces2</a>)
</body>
</html>
"""
def par(text: str = "") -> str:
"""Create an HTML paragraph."""
return f"<p>{text}</p>"
class TwillTest(Directory):
"""The actual test app."""
# Flask routes
_q_exports = (
"",
"logout",
"increment",
"incrementfail",
"restricted",
"login",
("test spaces", "test_spaces"),
"test_spaces",
"simpleform",
"getform",
"upload_file",
"http_auth",
"formpostredirect",
"exit",
"multisubmitform",
"exception",
"plaintext",
"xml",
"sleep",
"testform",
"testformaction",
"test_radiobuttons",
"test_refresh",
"test_refresh2",
"test_refresh3",
"test_refresh4",
"test_refresh5",
"test_checkbox",
"test_simple_checkbox",
"echo",
"test_checkboxes",
"test_global_form",
"two_forms",
"broken_form_1",
"broken_form_2",
"broken_form_3",
"broken_form_4",
"broken_form_5",
"broken_linktext",
"exit",
"display_post",
"display_environ",
)
def __init__(self) -> None:
"""Initialize the application."""
self.restricted = Restricted()
self.http_auth = HttpAuthRestricted()
@app.route("/")
def view_root() -> str:
"""Show index page."""
return message(session)
def exit(self) -> None:
"""Exit the application."""
raise SystemExit
def test_global_form(self) -> str:
"""Test the global form."""
return """
<html>
<head>
<title>Broken</title>
</head>
<body>
<div>
<input name="global_form_entry" type="text">
<input name="global_entry_2" type="text">
</div>
@app.route("/exception")
def view_exception() -> NoReturn:
"""Raise a server error."""
raise RuntimeError("500 error -- fail out!")
<form name="login" method="post">
<input type=text name=hello>
<input type=submit>
</form>
<form name="login" method="post"
action="http://iorich.caltech.edu:8080/display_post">
<input type=text name=hello>
<input type=submit>
</form>
@app.route("/test_spaces")
@app.route("/test spaces")
def view_test_spaces() -> str:
"""Test spaces."""
return "success"
</body>
</html>
"""
def display_post(self) -> str:
"""Show the form items."""
return "".join(
f"k: '''{k}''' : '''{v}'''<p>\n"
for k, v in get_request().form.items()
)
@app.route("/sleep")
def view_sleep() -> str:
"""Test timeouts."""
sleep(0.5)
return "sorry for the delay"
def display_environ(self) -> str:
"""Show the environment variables."""
return "".join(
f"k: '''{k}''' : '''{v}'''<p>\n"
for k, v in get_request().environ.items()
)
def _q_index(self) -> str:
"""Show index page."""
return message(get_session())
@app.route("/increment")
def view_increment() -> str:
"""Visit session."""
session["visit"] = session.get("visit", 0) + 1
return message(session)
def broken_form_1(self) -> str:
"""Get broken form 1."""
return """\
<form>
<input type=text name=blah value=thus>
"""
def broken_form_2(self) -> str:
"""Get broken form 2."""
return """\
<form>
<table>
<tr><td>
<input name='broken'>
</td>
</form>
</tr>
</form>
"""
@app.route("/incrementfail")
def view_incrementfail() -> NoReturn:
"""Visit session with failure."""
session["visit"] = session.get("visit", 0) + 1
raise RuntimeError(message(session))
def broken_form_3(self) -> str:
"""Get broken form 3."""
return """\
<table>
<tr><td>
<input name='broken'>
</td>
</form>
</tr>
</form>
"""
def broken_form_4(self) -> str:
"""Get broken form 4."""
return """\
<font>
<INPUT>
@app.route("/simpleform", methods=["GET", "POST"])
def view_simpleform() -> str:
"""Test non-existing submit button."""
s1, s2 = field(name="n"), field(name="n2")
values = par(" ".join(request.form.values()))
return f'{values}</p><form method="POST">{s1} {s2}</form>'
<FORM>
<input type="blah">
</form>
"""
def broken_form_5(self) -> str:
"""Get broken form 5."""
return """\
<div id="loginform">
<form method="post" name="loginform" action="ChkLogin">
<h3>ARINC Direct Login</h3>
<br/>
<strong>User ID</strong><br/>
<input name="username" id="username" type="text" style="width:80%"><br/>
<strong>Password</strong><br/>
<input name="password" type="password" style="width:80%"><br/>
<div id="buttonbar">
<input value="Login" name="login" class="button" type="submit">
</div>
</form>
</div>
"""
@app.route("/getform")
def view_getform() -> str:
"""Test form with get method."""
s = field("hidden", name="n", value="v")
return f'<form method="GET">{s}<input type="submit"></form>'
def broken_linktext(self) -> str:
"""Get broken link text."""
return """
<a href="/">
<span>some text</span>
</a>
"""
def test_refresh(self) -> str:
"""Test simple refresh."""
return """\
<meta http-equiv="refresh" content="2; url=./login">
hello, world.
"""
@app.route("/login", methods=["GET", "POST"])
def view_login() -> Any:
"""Test login."""
if request.method == "POST":
username = request.form.get("username")
if username:
session["user"] = username
return redirect("./")
def test_refresh2(self) -> str:
"""Test refresh with upper case."""
return """\
<META HTTP-EQUIV="REFRESH" CONTENT="2; URL=./login">
hello, world.
"""
login = field(name="username", value="")
s = field("submit", name="submit", value="submit me")
s2 = field("submit", name="nosubmit2", value="don't submit")
img = '<input type=image name="submit you" src="DNE.gif">'
def test_refresh3(self) -> str:
"""Test circular refresh."""
return """\
<meta http-equiv="refresh" content="2; url=./test_refresh3">
hello, world.
"""
return (
'<form method="POST"><p>Log in:</p>'
f"<p>{login}</p><p>{s2} {s}</p><p>{img}</p></form>"
)
def test_refresh4(self) -> str:
"""Test refresh together with similar meta tags."""
return """\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>o2.ie</title>
<meta http-equiv="refresh" content="0;URL=/login">
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body>
</body>
</html>
hello, world.
"""
def test_refresh5(self) -> str:
"""Check for situation where given URL is quoted."""
return """\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>o2.ie</title>
<meta http-equiv="refresh" content="0;'URL=/login'">
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body>
</body>
</html>
hello, world.
"""
@app.route("/logout")
def view_logout() -> Any:
"""Test logout."""
session.clear()
return redirect("./")
def exception(self) -> None:
"""Raise a server error."""
raise RuntimeError("500 error -- fail out!")
def test_spaces(self) -> str:
"""Test spaces."""
return "success"
@app.route("/upload_file", methods=["GET", "POST"])
def view_upload_file() -> str:
"""Test file upload."""
if request.method == "POST" and "upload" in request.files:
return request.files["upload"].read()
return """\
<form enctype="multipart/form-data" method="POST">
<input type="file" name="upload">
<input type="submit" value="submit">
</form>"""
def sleep(self) -> str:
"""Test timeouts."""
sleep(0.5)
return "sorry for the delay"
def increment(self) -> str:
"""Visit session."""
session = get_session()
session.visit += 1
return message(session)
@app.route("/formpostredirect", methods=["GET", "POST"])
def view_formpostredirect() -> Any:
"""Test redirect after a form POST."""
if request.method != "POST":
return """\
<form method="POST" enctype="multipart/form-data">
<input type="text" name="test">
<input type="submit" value="submit" name="submit">
</form>
"""
return redirect("./")
def incrementfail(self) -> str:
"""Visit session with failure."""
session = get_session()
session.visit += 1
raise RuntimeError(message(session))
def login(self) -> str:
"""Test login."""
request = get_request()
username_widget = widget.StringWidget(name="username", value="")
submit_widget = widget.SubmitWidget(name="submit", value="submit me")
submit_widget2 = widget.SubmitWidget(
name="nosubmit2", value="don't submit"
)
@app.route("/display_post", methods=["POST"])
def view_display_post() -> str:
"""Show the form items."""
return "".join(par(f"{k!s}: {v!r}") for k, v in request.form.items())
if request.form:
if submit_widget2.parse(request):
raise RuntimeError("Cannot parse request.")
username = username_widget.parse(request)
if username:
session = get_session()
session.set_user(username)
return redirect("./")
image_submit = """<input type=image name='submit you' src=DNE.gif>"""
@app.route("/display_environ")
def view_display_environ() -> str:
"""Show the environment variables."""
return "".join(par(f"{k!s}: {v!r}") for k, v in request.environ.items())
login = username_widget.render()
s, s2 = submit_widget.render(), submit_widget2.render()
img = image_submit
return f"<form method=POST>Log in: {login}<p>{s2}<p>{s}<p>{img}</form>"
def simpleform(self) -> str:
"""Test non-existing submit button."""
request = get_request()
@app.route("/echo")
def view_echo() -> str:
"""Show query parameters."""
show = ", ".join(f"{k}={v}" for k, v in request.args.items()) or "nothing"
return f"<html><body>{show}</body></html>"
s1 = widget.StringWidget(name="n", value="").parse(request)
s2 = widget.StringWidget(name="n2", value="").parse(request)
return (
f"{s1} {s2} "
"<form method=POST>"
"<input type=text name=n><input type=text name=n2>"
"</form>"
)
@app.route("/plaintext")
def view_plaintext() -> Response:
"""Test plain text response."""
response = make_response("hello, world")
response.headers["Content-Type"] = "text/plain"
return response
def getform(self) -> str:
"""Test form with get method."""
return (
"<form method=GET><input type=hidden name=n value=v>"
"<input type=submit value=send></form>"
)
def multisubmitform(self) -> str:
"""Test form with multiple submit buttons."""
request = get_request()
@app.route("/xml")
def view_xml() -> Response:
"""Test XML response."""
response = make_response(
'<?xml version="1.0" encoding="utf-8" ?><foo>bår</foo>'
)
response.headers["Content-Type"] = "text/xml"
return response
submit1 = widget.SubmitWidget("sub_a", value="sub_a")
submit2 = widget.SubmitWidget("sub_b", value="sub_b")
s = ""
if request.form:
used = False
if submit1.parse(request):
used = True
s += "used_sub_a"
if submit2.parse(request):
used = True
s += "used_sub_b"
@app.route("/restricted")
def view_restricted() -> str:
"""Test restricted access."""
if "user" not in session:
response = make_response("you must have a username", 403)
abort(response)
return "you made it!"
if not used:
raise RuntimeError("Not button was used.")
# print out the referer, too.
referer = request.environ.get("HTTP_REFERER")
if referer:
s += f"<p>referer: {referer}"
@app.route("/http_auth")
def view_http_auth() -> str:
"""Test restricted access using HTTP authentication."""
login = passwd = None
ha = request.environ.get("HTTP_AUTHORIZATION")
if ha:
auth_type, auth_string = ha.split(None, 1)
if auth_type.lower() == "basic":
auth_string = decodebytes(auth_string.encode("utf-8"))
login_bytes, passwd_bytes = auth_string.split(b":", 1)
login = login_bytes.decode("utf-8")
passwd = passwd_bytes.decode("utf-8")
if (login, passwd) != ("test", "password"):
passwd = None
s1, s2 = submit1.render(), submit2.render()
return f"<form method=POST>{s} {s1} {s2}</form>"
if passwd:
print(f"Successful login as {login}")
elif login:
print(f"Invalid login attempt as {login}")
else:
print("Access has been denied")
print()
if not passwd:
response = make_response(
"you are not authorized to access this resource", 401
)
response.headers["WWW-Authenticate"] = 'Basic realm="Protected"'
abort(response)
return "you made it!"
def testformaction(self) -> str:
"""Test form actions."""
request = get_request()
keys = sorted(k for k in request.form if request.form[k])
return "==" + " AND ".join(keys) + "=="
def testform(self) -> str:
"""Test form."""
request = get_request()
@app.route("/broken_linktext")
def view_broken_linktext() -> str:
"""Get broken link text."""
return """
<a href="/">
<span>some text</span>
</a>
"""
s = ""
if not request.form:
s = "NO FORM"
if request.form and "selecttest" in request.form:
values = request.form["selecttest"]
if isinstance(values, str):
values = [values]
values = " AND ".join(values)
s += f"SELECTTEST: =={values}==<p>"
@app.route("/broken_form_1")
def view_broken_form_1() -> str:
"""Get broken form 1."""
return """\
<form>
<input type="text" name="blah" value="thus">
"""
if request.form:
items = []
for name in ("item", "item_a", "item_b", "item_c"):
if request.form.get(name):
value = request.form[name]
items.append(f"{name}={value}")
values = " AND ".join(items)
s += f"NAMETEST: =={values}==<p>"
return f"""\
{s}
<form method=POST id=the_form>
<select name=selecttest multiple>
<option> val
<option value='selvalue1'> value1 </option>
<option value='selvalue2'> value2 </option>
<option value='selvalue3'> value3 </option>
<option value='test.value3'> testme.val </option>
<option value=Test.Value4> testme4.val </option>
</select>
@app.route("/broken_form_2")
def view_broken_form_2() -> str:
"""Get broken form 2."""
return """\
<form>
<table>
<tr><td>
<input name="broken">
</td>
</form>
</tr>
</form>
"""
<input type=text name=item>
<input type=text name=item_a>
<input type=text name=item_b>
<input type=text name=item_c>
<input type=text id=some_id>
<input type=submit value=post id=submit_button>
@app.route("/broken_form_3")
def view_broken_form_3() -> str:
"""Get broken form 3."""
return """\
<table>
<tr><td>
<input name="broken">
</td>
</form>
</tr>
</form>
"""
def two_forms(self) -> str:
"""Test two forms."""
request = get_request()
if request.form:
form = request.form.get("form")
item = request.form.get("item")
s = f"FORM={form} ITEM={item}"
else:
s = "NO FORM"
@app.route("/broken_form_4")
def view_broken_form_4() -> str:
"""Get broken form 4."""
return """\
<font>
<INPUT>
return f"""\
<h1>Two Forms</h1>
<p>== {s} ==</p>
<form method=POST id=form1>
<input type=text name=item>
<input type=hidden name=form value=1>
<input type=submit value=post>
<FORM>
<input type="blah">
</form>
<form method=POST id=form2>
<input type=text name=item>
<input type=hidden name=form value=2>
<input type=submit value=post>
"""
@app.route("/broken_form_5")
def view_broken_form_5() -> str:
"""Get broken form 5."""
return """\
<div id="loginform">
<form method="post" name="loginform" action="ChkLogin">
<h3>ARINC Direct Login</h3>
<br/>
<strong>User ID</strong><br/>
<input name="username" id="username" type="text" style="width:80%"><br/>
<strong>Password</strong><br/>
<input name="password" type="password" style="width:80%"><br/>
<div id="buttonbar">
<input value="Login" name="login" class="button" type="submit">
</div>
</form>
</div>
"""
def test_checkbox(self) -> str:
"""Test single checkbox."""
request = get_request()
s = ""
if request.form and "checkboxtest" in request.form:
value = request.form["checkboxtest"]
if not isinstance(value, str):
value = value[0]
@app.route("/test_checkbox", methods=["GET", "POST"])
def view_test_checkbox() -> str:
"""Test single checkbox."""
s = ""
if request.method == "POST" and "checkboxtest" in request.form:
value = request.form["checkboxtest"]
s += f"CHECKBOXTEST: =={value}==<p>"
s += par(f"CHECKBOXTEST: =={value}==")
return f"""\
return f"""\
{s}
<form method=POST>
<form method="POST">

@@ -542,183 +375,270 @@ <input type="checkbox" name="checkboxtest" value="True">

<input type=submit value=post>
<input type="submit" value="post">
</form>
"""
def test_checkboxes(self) -> str:
"""Test multiple checkboxes."""
request = get_request()
s = ""
if request.form and "checkboxtest" in request.form:
value = request.form["checkboxtest"]
if not isinstance(value, str):
value = ",".join(value)
@app.route("/test_checkboxes", methods=["GET", "POST"])
def view_test_checkboxes() -> str:
"""Test multiple checkboxes."""
s = ""
if request.method == "POST" and "checkboxtest" in request.form:
value = ",".join(request.form.getlist("checkboxtest"))
s += f"CHECKBOXTEST: =={value}==<p>"
s += par(f"CHECKBOXTEST: =={value}==")
return f"""\
return f"""\
{s}
<form method=POST>
<form method="POST">
<input type="checkbox" name="checkboxtest" value="one">
<input type="checkbox" name="checkboxtest" value="two">
<input type="checkbox" name="checkboxtest" value="three">
<input type=submit value=post>
<input type="submit" value="post">
</form>
"""
def test_simple_checkbox(self) -> str:
"""Test simple checkbox."""
request = get_request()
s = ""
if request.form and "checkboxtest" in request.form:
value = request.form["checkboxtest"]
if not isinstance(value, str):
value = value[0]
@app.route("/test_simple_checkbox", methods=["GET", "POST"])
def view_test_simple_checkbox() -> str:
"""Test simple checkbox."""
s = ""
if request.method == "POST" and "checkboxtest" in request.form:
value = request.form["checkboxtest"]
s += f"CHECKBOXTEST: =={value}==<p>"
s += par(f"CHECKBOXTEST: =={value}==")
return f"""\
return f"""\
{s}
<form method=POST>
<form method="POST">
<input type="checkbox" name="checkboxtest">
<input type=submit value=post>
<input type="submit" value="post">
</form>
"""
def test_radiobuttons(self) -> str:
"""Test radio buttons."""
request = get_request()
s = ""
if request.form and "radiobuttontest" in request.form:
value = request.form["radiobuttontest"]
if not isinstance(value, str):
value = ",".join(value)
@app.route("/test_radiobuttons", methods=["GET", "POST"])
def view_test_radiobuttons() -> str:
"""Test radio buttons."""
s = ""
if request.method == "POST" and "radiobuttontest" in request.form:
value = request.form["radiobuttontest"]
s += f"RADIOBUTTONTEST: =={value}==<p>"
s += par(f"RADIOBUTTONTEST: =={value}==")
return f"""\
return f"""\
{s}
<form method=POST>
<input type="radio" name="radiobuttontest" value="one">
<input type="radio" name="radiobuttontest" value="two">
<input type="radio" name="radiobuttontest" value="three">
<input type=submit value=post>
</form>
"""
<form method="POST">
<input type="radio" name="radiobuttontest" value="one">
<input type="radio" name="radiobuttontest" value="two">
<input type="radio" name="radiobuttontest" value="three">
<input type="submit" value="post">
</form>
"""
def formpostredirect(self) -> str:
"""Test redirect after a form POST."""
request = get_request()
if not request.form:
return """\
<form method=POST enctype=multipart/form-data>
<input type=text name=test>
<input type=submit value=submit name=submit>
@app.route("/testformaction", methods=["POST"])
def view_testformaction() -> str:
"""Test form actions."""
keys = sorted(k for k in request.form if request.form[k])
return "==" + " AND ".join(keys) + "=="
@app.route("/testform", methods=["GET", "POST"])
def view_testform() -> str:
"""Test form."""
s = ""
if not request.form:
s = "NO FORM"
if request.form and "selecttest" in request.form:
values = " AND ".join(request.form.getlist("selecttest"))
s += par(f"SELECTTEST: =={values}==")
if request.form:
items = []
for name in ("item", "item_a", "item_b", "item_c"):
if request.form.get(name):
value = request.form[name]
items.append(f"{name}={value}")
values = " AND ".join(items)
s += par(f"NAMETEST: =={values}==")
return f"""\
{s}
<form method="POST" id="the_form">
<select name="selecttest" multiple>
<option>val</option>
<option value="selvalue1">value1</option>
<option value="selvalue2">value2</option>
<option value="selvalue3">value3</option>
<option value="test.value3">testme.val</option>
<option value="Test.Value4">testme4.val</option>
</select>
<input type="text" name="item">
<input type="text" name="item_a">
<input type="text" name="item_b">
<input type="text" name="item_c">
<input type="text" id="some_id">
<input type="submit" value="post" id="submit_button">
</form>
"""
return redirect(get_path(1) + "/")
def logout(self) -> str:
"""Test logout."""
get_session_manager().expire_session()
return redirect(get_path(1) + "/") # back to index page
def plaintext(self) -> str:
"""Test plain text response."""
response = get_response()
response.set_content_type("text/plain")
return "hello, world"
@app.route("/multisubmitform", methods=["GET", "POST"])
def view_multisubmitform() -> str:
"""Test form with multiple submit buttons."""
s1 = field("submit", "sub_a", value="sub_a")
s2 = field("submit", "sub_b", value="sub_b")
def xml(self) -> str:
"""Test XML response."""
response = get_response()
response.set_content_type("text/xml")
return '<?xml version="1.0" encoding="utf-8" ?><foo>bår</foo>'
s = ""
if request.method == "POST":
used = False
if "sub_a" in request.form:
used = True
s += "used_sub_a"
if "sub_b" in request.form:
used = True
s += "used_sub_b"
def echo(self) -> str:
"""Show form content."""
request = get_request()
if request.form and "q" in request.form:
return request.form["q"]
return "<html><body>No Content</body></html>"
if not used:
raise RuntimeError("Not button was used.")
def upload_file(self) -> str:
"""Test file upload."""
request = get_request()
if request.form:
return request.form["upload"].fp.read()
return """\
<form enctype=multipart/form-data method=POST>
<input type=file name=upload>
<input type=submit value=submit>
</form>"""
referer = request.environ.get("HTTP_REFERER")
if referer:
s += par(f"referer: {referer}")
return f'<form method="POST">{s} {s1} {s2}</form>'
class Restricted(AccessControlled, Directory):
"""A directory with restricted access."""
_q_exports = ("",)
@app.route("/two_forms", methods=["GET", "POST"])
def view_two_forms() -> str:
"""Test two forms."""
if request.form:
form = request.form.get("form")
item = request.form.get("item")
s = f"FORM={form} ITEM={item}"
else:
s = "NO FORM"
def _q_access(self) -> None:
"""Check access."""
session = get_session()
if not session.user:
raise AccessError("you must have a username")
return f"""\
<h1>Two Forms</h1>
<p>== {s} ==</p>
<form method="POST" id="form1">
<input type="text" name="item">
<input type="hidden" name="form" value="1">
<input type="submit" value="post">
</form>
<form method="POST" id="form2">
<input type="text" name="item">
<input type="hidden" name="form" value="2">
<input type="submit" value="post">
</form>
"""
def _q_index(self) -> str:
"""Show index page."""
return "you made it!"
@app.route("/test_global_form", methods=["GET", "POST"])
def view_test_global_form() -> str:
"""Test the global form."""
return """
<html>
<head>
<title>Broken</title>
</head>
<body>
<div>
<input name="global_form_entry" type="text">
<input name="global_entry_2" type="text">
</div>
class HttpAuthRestricted(AccessControlled, Directory):
"""A directory with restricted access using Basic Authentication."""
<form name="login" method="post" action="display_post">
<input type="text" name="hello">
<input type="hidden" name="form" value="1">
<input type="submit">
</form>
_q_exports = ("",)
<form name="login" method="post" action="display_post">
<input type="text" name="hello">
<input type="hidden" name="form" value="2">
<input type="submit">
</form>
def _q_access(self) -> None:
"""Check access."""
r = get_request()
</body>
</html>
"""
login = passwd = None
ha = r.get_environ("HTTP_AUTHORIZATION", None)
if ha:
auth_type, auth_string = ha.split(None, 1)
if auth_type.lower() == "basic":
auth_string = decodebytes(auth_string.encode("utf-8"))
login, passwd = auth_string.split(b":", 1)
login = login.decode("utf-8")
passwd = passwd.decode("utf-8")
if (login, passwd) != ("test", "password"):
passwd = None
if passwd:
print(f"Successful login as '{login}'")
elif login:
print(f"Invalid login attempt as '{login}'")
else:
print("Access has been denied")
print()
if not passwd:
raise UnauthorizedError
@app.route("/test_refresh")
def view_test_refresh() -> str:
"""Test simple refresh."""
return """\
<meta http-equiv="refresh" content="2; url=./login">
hello, world.
"""
def _q_index(self) -> str:
"""Show index page."""
return "you made it!"
@app.route("/test_refresh2")
def view_test_refresh2() -> str:
"""Test refresh with upper case."""
return """\
<META HTTP-EQUIV="REFRESH" CONTENT="2; URL=./login">
hello, world.
"""
@app.route("/test_refresh3")
def view_test_refresh3() -> str:
"""Test circular refresh."""
return """\
<meta http-equiv="refresh" content="2; url=./test_refresh3">
hello, world.
"""
@app.route("/test_refresh4")
def view_test_refresh4() -> str:
"""Test refresh together with similar meta tags."""
return """\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>o2.ie</title>
<meta http-equiv="refresh" content="0;URL=/login">
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body>
</body>
</html>
hello, world.
"""
@app.route("/test_refresh5")
def view_test_refresh5() -> str:
"""Check for situation where given URL is quoted."""
return """\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>o2.ie</title>
<meta http-equiv="refresh" content="0;'URL=/login'">
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body>
</body>
</html>
hello, world.
"""
if __name__ == "__main__":
from quixote.server.simple_server import ( # type: ignore[import-untyped]
run,
)
port = int(os.environ.get("TWILL_TEST_PORT", PORT))
print(f"starting twill test server on port {port}.")
try:
run(create_publisher, host=HOST, port=port)
app.run(host=HOST, port=port, debug=DEBUG, use_reloader=RELOAD)
except KeyboardInterrupt:
print("Keyboard interrupt ignored.")

@@ -30,4 +30,6 @@ from twill import browser, commands

assert set(browser.form().inputs.keys()) == set(
"username password login".split()
), "should get proper fields"
assert set(browser.form().inputs.keys()) == {
"username",
"password",
"login",
}, "should get proper fields"
import pytest
import twill

@@ -48,3 +49,3 @@ from twill import browser, commands, namespaces

with pytest.raises(TwillException):
browser.title # noqa: B018s
browser.title # noqa: B018

@@ -51,0 +52,0 @@ commands.go(url)

import pytest
import twill

@@ -3,0 +4,0 @@ from twill import commands, namespaces

@@ -6,2 +6,3 @@ import sys

import pytest
import twill.parse

@@ -17,3 +18,3 @@ from twill import commands, namespaces

def setup_module():
global _log_commands # noqa: PLW0603
global _log_commands
_log_commands = twill.parse.log_commands(True) # noqa: FBT003

@@ -20,0 +21,0 @@

@@ -7,2 +7,3 @@ """Test a boatload of miscellaneous functionality."""

import pytest
from twill import browser, commands

@@ -9,0 +10,0 @@ from twill.errors import TwillException

import pytest
from twill import browser, commands, namespaces

@@ -3,0 +4,0 @@ from twill.errors import TwillException

@@ -7,2 +7,3 @@ """Same as test_basic, but using the command interpreter."""

import pytest
from twill import __url__ as twill_url

@@ -28,3 +29,3 @@ from twill import __version__ as twill_version

">> exit [<code>]",
' Exit twill with given exit code (default 0, "no error").',
'Exit twill with given exit code (default 0, "no error").',
"Print version information.",

@@ -31,0 +32,0 @@ "Imported extension module 'shell_test'.",

import pytest
from twill import commands

@@ -3,0 +4,0 @@ from twill.errors import TwillException

@@ -8,2 +8,3 @@ """Test the utils.run_tidy function.

import pytest
from twill import utils

@@ -10,0 +11,0 @@ from twill.commands import config

@@ -5,2 +5,3 @@ from time import sleep

from httpx import ReadTimeout
from twill import commands

@@ -7,0 +8,0 @@

import pytest
from twill import commands

@@ -3,0 +4,0 @@ from twill.errors import TwillException

"""Test the unit-test support framework using (naturally) a unit test."""
import os
import time
from pathlib import Path
import twill.unit
from quixote.server.simple_server import ( # type: ignore[import-untyped]
run as quixote_run,
)
from .server import create_publisher
from .server import app
from .utils import test_dir
PORT = 8081 # default port to run the server on
SLEEP = 0.5 # time to wait for the server to start
HOST = "127.0.0.1"
PORT = 8080
SLEEP = 0.25
DEBUG = False
RELOAD = False
def run_server(port: int = PORT) -> None:
def run_server() -> None:
"""Function to run the server"""
quixote_run(create_publisher, port=port)
port = int(os.environ.get("TWILL_TEST_PORT", PORT))
app.run(host=HOST, port=port, debug=DEBUG, use_reloader=RELOAD)
def test():
"""The unit test"""
def just_wait() -> None:
"""Just wait instead of starting the server."""
while True:
time.sleep(SLEEP or 1)
def test(url: str) -> None:
"""Test wrapper for unit support."""
# abspath to the script
script = str(Path(test_dir, "test_unit_support.twill"))
# create test_info object
test_info = twill.unit.TestInfo(script, run_server, PORT, SLEEP)
# the port that shall be used for running the server
port = int(os.environ.get("TWILL_TEST_PORT", PORT))
# run tests!
twill.unit.run_test(test_info)
# create a TestInfo object
test_info = twill.unit.TestInfo(script, run_server, HOST, port, SLEEP)
if test_info.url == url:
# server already running, run a dummy server function that just waits
test_info = twill.unit.TestInfo(script, just_wait, HOST, port, SLEEP)
if __name__ == "__main__":
test()
# run the tests!
twill.unit.run_test(test_info)
from pathlib import Path
import pytest
from twill import utils

@@ -5,0 +6,0 @@ from twill.errors import TwillException

import pytest
from twill import parse

@@ -3,0 +4,0 @@

@@ -12,3 +12,2 @@ """Utility functions for testing twill."""

import httpx
import twill

@@ -23,6 +22,7 @@

START = True # whether to automatically start the quixote server
START = True # whether to automatically start the test server
LOG = None # name of the server log file or None
_url = None # current server url
__popen__ = None # server process

@@ -114,3 +114,3 @@

Run a Quixote simple_server on HOST:PORT with subprocess.
Run a Flask development server on HOST:PORT with subprocess.
All output is captured and thrown away.

@@ -120,3 +120,3 @@

"""
global _url # noqa: PLW0603
global _url, __popen__

@@ -127,7 +127,8 @@ if port is None:

if START:
out = open(LOG or os.devnull, "w", buffering=1) # noqa: SIM115
log = LOG or os.devnull
out = open(log, "w", buffering=1, encoding="utf-8") # noqa: SIM115
print( # noqa: T201
"Starting:", sys.executable, "tests/server.py", Path.cwd()
)
subprocess.Popen( # noqa: S603
__popen__ = subprocess.Popen( # noqa: S603
[sys.executable, "-u", "server.py"],

@@ -140,3 +141,2 @@ stderr=subprocess.STDOUT,

# noinspection HttpUrlsUsage
_url = f"http://{HOST}:{port}/"

@@ -147,11 +147,13 @@

"""Stop a previously started test web server."""
global _url # noqa: PLW0603
global _url, __popen__
if _url:
if __popen__:
if START:
try:
httpx.get(f"{_url}exit", timeout=10)
__popen__.terminate()
except Exception as error: # noqa: BLE001
print("ERROR:", error) # noqa: T201
print("Could not stop the server.") # noqa: T201
_url = None
else:
print("The server has been stopped.") # noqa: T201
_url = __popen__ = None
+10
-10
[tox]
envlist = py3{8,9,10,11,12}, ruff, mypy, docs, manifest
envlist = py3{8,9,10,11,12,13,14rc2}, ruff, mypy, docs, manifest
[testenv:ruff]
basepython = python3.12
deps = ruff>=0.5,<0.6
basepython = python3.13
deps = ruff>=0.12,<0.13
commands =

@@ -12,6 +12,6 @@ ruff check src/twill tests extras

[testenv:mypy]
basepython = python3.12
basepython = python3.13
deps =
mypy >= 1.10, <1.11
dnspython >=2.5, <3
mypy >= 1.17.1, <1.18
dnspython >=2.7, <3
types-lxml

@@ -23,4 +23,4 @@ types-setuptools

[testenv:docs]
basepython = python3.12
deps = sphinx >=7.3, <8
basepython = python3.13
deps = sphinx >=8.2, <9
extras =

@@ -32,4 +32,4 @@ docs

[testenv:manifest]
basepython = python3.12
deps = check-manifest>=0.49,<1
basepython = python3.13
deps = check-manifest>=0.50,<1
commands =

@@ -36,0 +36,0 @@ check-manifest -v

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet