Merge pull request #310 from hartym/kasei-exclusive-locks

MR from Kasei : exclusive locks
This commit is contained in:
Romain Dorgueil
2019-05-16 15:07:30 +02:00
committed by GitHub
23 changed files with 216 additions and 261 deletions

View File

@ -1,7 +1,8 @@
# Generated by Medikit 0.6.3 on 2018-08-11.
# Generated by Medikit 0.7.1 on 2019-05-16.
# All changes will be overriden.
# Edit Projectfile and run “make update” (or “medikit update”) to regenerate.
PACKAGE ?= bonobo
PYTHON ?= $(shell which python || echo python)
PYTHON_BASENAME ?= $(shell basename $(PYTHON))
@ -29,7 +30,7 @@ SPHINX_BUILDDIR ?= $(SPHINX_SOURCEDIR)/_build
SPHINX_AUTOBUILD ?= $(PYTHON_DIRNAME)/sphinx-autobuild
MEDIKIT ?= $(PYTHON) -m medikit
MEDIKIT_UPDATE_OPTIONS ?=
MEDIKIT_VERSION ?= 0.6.3
MEDIKIT_VERSION ?= 0.7.1
.PHONY: $(SPHINX_SOURCEDIR) clean format help install install-dev install-docker install-jupyter install-sqlalchemy medikit quick test update update-requirements watch-$(SPHINX_SOURCEDIR)
@ -135,5 +136,5 @@ update-requirements: ## Update project artifacts using medikit, including requ
help: ## Shows available commands.
@echo "Available commands:"
@echo
@grep -E '^[a-zA-Z_-]+:.*?##[\s]?.*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?##"}; {printf " make \033[36m%-30s\033[0m %s\n", $$1, $$2}'
@grep -E '^[a-zA-Z_-]+:.*?##[\s]?.*$$' --no-filename $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?##"}; {printf " make \033[36m%-30s\033[0m %s\n", $$1, $$2}'
@echo

View File

@ -47,7 +47,7 @@ python.add_requirements(
'graphviz >=0.8,<0.9',
'jinja2 ~=2.9',
'mondrian ~=0.8',
'packaging ~=17.0',
'packaging ~=19.0',
'psutil ~=5.4',
'python-slugify ~=1.2.0',
'requests ~=2.0',

View File

@ -8,50 +8,17 @@
import sys
from pathlib import Path
from bonobo._api import (
CsvReader, CsvWriter, FileReader, FileWriter, Filter, FixedWindow, Format, Graph, JsonReader, JsonWriter,
LdjsonReader, LdjsonWriter, Limit, MapFields, OrderFields, PickleReader, PickleWriter, PrettyPrinter, RateLimited,
Rename, SetFields, Tee, UnpackItems, __all__, __doc__, count, create_reader, create_strategy, create_writer,
get_argument_parser, get_examples_path, identity, inspect, noop, open_examples_fs, open_fs, parse_args, run
)
from bonobo._version import __version__
if sys.version_info < (3, 5):
raise RuntimeError("Python 3.5+ is required to use Bonobo.")
from bonobo._api import (
run,
inspect,
Graph,
create_strategy,
open_fs,
CsvReader,
CsvWriter,
FileReader,
FileWriter,
Filter,
FixedWindow,
Format,
JsonReader,
JsonWriter,
LdjsonReader,
LdjsonWriter,
Limit,
MapFields,
OrderFields,
PickleReader,
PickleWriter,
PrettyPrinter,
RateLimited,
Rename,
SetFields,
Tee,
UnpackItems,
count,
identity,
noop,
create_reader,
create_writer,
get_examples_path,
open_examples_fs,
get_argument_parser,
parse_args,
__all__,
__doc__,
)
from bonobo._version import __version__
__all__ = ["__version__"] + __all__
with (Path(__file__).parent / "bonobo.svg").open() as f:

View File

@ -2,6 +2,7 @@ import argparse
import logging
import mondrian
from bonobo import settings
from bonobo.commands.base import BaseCommand, BaseGraphCommand

View File

@ -5,10 +5,10 @@ configurable transformations, either class-based or function-based.
"""
from bonobo.config.configurables import Configurable
from bonobo.config.functools import transformation_factory, partial
from bonobo.config.functools import partial, transformation_factory
from bonobo.config.options import Method, Option
from bonobo.config.processors import ContextProcessor, use_context, use_context_processor, use_raw_input, use_no_input
from bonobo.config.services import Container, Exclusive, Service, use, create_container
from bonobo.config.processors import ContextProcessor, use_context, use_context_processor, use_no_input, use_raw_input
from bonobo.config.services import Container, Exclusive, Service, create_container, use
from bonobo.util import deprecated_alias
requires = deprecated_alias("requires", use)

View File

@ -146,14 +146,16 @@ class Exclusive(ContextDecorator):
"""
_locks = {}
_locks_creation_lock = threading.Lock()
def __init__(self, wrapped):
self._wrapped = wrapped
def get_lock(self):
_id = id(self._wrapped)
if not _id in Exclusive._locks:
Exclusive._locks[_id] = threading.RLock()
with Exclusive._locks_creation_lock:
if not _id in Exclusive._locks:
Exclusive._locks[_id] = threading.RLock()
return Exclusive._locks[_id]
def __enter__(self):

View File

@ -6,7 +6,7 @@ This module contains all tools for Bonobo and Django to interract nicely.
"""
from .utils import create_or_update
from .commands import ETLCommand
from .utils import create_or_update
__all__ = ["ETLCommand", "create_or_update"]

View File

@ -2,13 +2,13 @@ from logging import getLogger
from types import GeneratorType
from colorama import Back, Fore, Style
from django.core.management import BaseCommand
from django.core.management.base import OutputWrapper
from mondrian import term
import bonobo
from bonobo.plugins.console import ConsoleOutputPlugin
from bonobo.util.term import CLEAR_EOL
from django.core.management import BaseCommand
from django.core.management.base import OutputWrapper
from .utils import create_or_update

View File

@ -1,14 +1,15 @@
import os
# https://developers.google.com/api-client-library/python/guide/aaa_oauth
# pip install google-api-python-client (1.6.4)
import httplib2
from apiclient import discovery
from oauth2client import client, tools
from oauth2client.file import Storage
from oauth2client.tools import argparser
# https://developers.google.com/api-client-library/python/guide/aaa_oauth
# pip install google-api-python-client (1.6.4)
HOME_DIR = os.path.expanduser("~")
GOOGLE_SECRETS = os.path.join(HOME_DIR, ".cache/secrets/client_secrets.json")

View File

@ -3,8 +3,8 @@ from urllib.parse import urlencode
import requests # todo: make this a service so we can substitute it ?
from bonobo.config import Option
from bonobo.config.processors import ContextProcessor
from bonobo.config.configurables import Configurable
from bonobo.config.processors import ContextProcessor
from bonobo.util.objects import ValueHolder

View File

@ -1,7 +1,7 @@
import os
import bonobo
from bonobo.execution.strategies import STRATEGIES, DEFAULT_STRATEGY
from bonobo.execution.strategies import DEFAULT_STRATEGY, STRATEGIES
from bonobo.util.statistics import Timer

View File

@ -7,9 +7,7 @@ at home if you want to give it a shot.
"""
from bonobo.execution.strategies.executor import (
ProcessPoolExecutorStrategy,
ThreadPoolExecutorStrategy,
AsyncThreadPoolExecutorStrategy,
AsyncThreadPoolExecutorStrategy, ProcessPoolExecutorStrategy, ThreadPoolExecutorStrategy
)
from bonobo.execution.strategies.naive import NaiveStrategy

View File

@ -330,6 +330,7 @@ def MapFields(function, key=True):
:param key: bool or callable
:return: callable
"""
@use_raw_input
def _MapFields(bag):
try:
@ -342,12 +343,10 @@ def MapFields(function, key=True):
fields = bag._fields
except AttributeError as e:
raise UnrecoverableAttributeError(
'This transformation works only on objects with named'
' fields (namedtuple, BagType, ...).') from e
"This transformation works only on objects with named" " fields (namedtuple, BagType, ...)."
) from e
return factory(
function(value) if key(key_) else value for key_, value in zip(fields, bag)
)
return factory(function(value) if key(key_) else value for key_, value in zip(fields, bag))
elif key:
return factory(function(value) for value in bag)
else:

View File

@ -6,17 +6,9 @@ and inspect transformations, graphs, and nodes.
from bonobo.util.collections import cast, ensure_tuple, sortedlist, tuplize
from bonobo.util.compat import deprecated, deprecated_alias
from bonobo.util.inspect import (
inspect_node,
isconfigurable,
isconfigurabletype,
iscontextprocessor,
isdict,
ismethod,
isoption,
istuple,
istype,
inspect_node, isconfigurable, isconfigurabletype, iscontextprocessor, isdict, ismethod, isoption, istuple, istype
)
from bonobo.util.objects import get_name, get_attribute_or_create, ValueHolder
from bonobo.util.objects import ValueHolder, get_attribute_or_create, get_name
# Bonobo's util API
__all__ = [

View File

@ -1,40 +1,40 @@
-e .[dev]
-r requirements.txt
alabaster==0.7.11
arrow==0.12.1
atomicwrites==1.1.5
attrs==18.1.0
alabaster==0.7.12
arrow==0.13.1
atomicwrites==1.3.0
attrs==19.1.0
babel==2.6.0
binaryornot==0.4.4
certifi==2018.4.16
certifi==2019.3.9
chardet==3.0.4
click==6.7
click==7.0
cookiecutter==1.5.1
coverage==4.5.1
coverage==4.5.3
docutils==0.14
future==0.16.0
idna==2.7
imagesize==1.0.0
future==0.17.1
idna==2.8
imagesize==1.1.0
jinja2-time==0.2.0
jinja2==2.10
markupsafe==1.0
more-itertools==4.3.0
packaging==17.1
pluggy==0.7.1
poyo==0.4.1
py==1.5.4
pygments==2.2.0
pyparsing==2.2.0
pytest-cov==2.5.1
pytest-timeout==1.3.1
pytest==3.7.1
python-dateutil==2.7.3
pytz==2018.5
requests==2.19.1
six==1.11.0
jinja2==2.10.1
markupsafe==1.1.1
more-itertools==7.0.0
packaging==19.0
pluggy==0.11.0
poyo==0.4.2
py==1.8.0
pygments==2.4.0
pyparsing==2.4.0
pytest-cov==2.7.1
pytest-timeout==1.3.3
pytest==3.10.1
python-dateutil==2.8.0
pytz==2019.1
requests==2.21.0
six==1.12.0
snowballstemmer==1.2.1
sphinx-sitemap==0.2
sphinx==1.7.6
sphinx==1.8.5
sphinxcontrib-websupport==1.1.0
urllib3==1.23
whichcraft==0.4.1
urllib3==1.24.3
whichcraft==0.5.2

View File

@ -2,28 +2,29 @@
-r requirements.txt
appdirs==1.4.3
bonobo-docker==0.6.0
certifi==2018.4.16
cached-property==1.5.1
certifi==2019.3.9
chardet==3.0.4
colorama==0.3.9
docker-pycreds==0.3.0
docker-pycreds==0.4.0
docker==2.7.0
fs==2.0.27
fs==2.4.5
graphviz==0.8.4
idna==2.7
jinja2==2.10
markupsafe==1.0
idna==2.8
jinja2==2.10.1
markupsafe==1.1.1
mondrian==0.8.0
packaging==17.1
pbr==4.2.0
psutil==5.4.6
pyparsing==2.2.0
python-slugify==1.2.5
pytz==2018.5
requests==2.19.1
pbr==5.2.0
psutil==5.6.2
pyparsing==2.4.0
python-slugify==1.2.6
pytz==2019.1
requests==2.21.0
semantic-version==2.6.0
six==1.11.0
stevedore==1.29.0
unidecode==1.0.22
urllib3==1.23
websocket-client==0.48.0
six==1.12.0
stevedore==1.30.1
unidecode==1.0.23
urllib3==1.24.3
websocket-client==0.56.0
whistle==1.0.1

View File

@ -1,44 +1,45 @@
-e .[jupyter]
-r requirements.txt
appnope==0.1.0
attrs==19.1.0
backcall==0.1.0
bleach==2.1.3
decorator==4.3.0
entrypoints==0.2.3
html5lib==1.0.1
ipykernel==4.8.2
bleach==3.1.0
decorator==4.4.0
defusedxml==0.6.0
entrypoints==0.3
ipykernel==5.1.1
ipython-genutils==0.2.0
ipython==6.5.0
ipython==7.5.0
ipywidgets==6.0.1
jedi==0.12.1
jinja2==2.10
jsonschema==2.6.0
jupyter-client==5.2.3
jupyter-console==5.2.0
jedi==0.13.3
jinja2==2.10.1
jsonschema==3.0.1
jupyter-client==5.2.4
jupyter-console==6.0.0
jupyter-core==4.4.0
jupyter==1.0.0
markupsafe==1.0
mistune==0.8.3
nbconvert==5.3.1
markupsafe==1.1.1
mistune==0.8.4
nbconvert==5.5.0
nbformat==4.4.0
notebook==5.6.0
notebook==5.7.8
pandocfilters==1.4.2
parso==0.3.1
pexpect==4.6.0
pickleshare==0.7.4
prometheus-client==0.3.1
prompt-toolkit==1.0.15
parso==0.4.0
pexpect==4.7.0
pickleshare==0.7.5
prometheus-client==0.6.0
prompt-toolkit==2.0.9
ptyprocess==0.6.0
pygments==2.2.0
python-dateutil==2.7.3
pyzmq==17.1.2
qtconsole==4.3.1
pygments==2.4.0
pyrsistent==0.15.2
python-dateutil==2.8.0
pyzmq==18.0.1
qtconsole==4.4.4
send2trash==1.5.0
simplegeneric==0.8.1
six==1.11.0
terminado==0.8.1
testpath==0.3.1
tornado==5.1
six==1.12.0
terminado==0.8.2
testpath==0.4.2
tornado==6.0.2
traitlets==4.3.2
wcwidth==0.1.7
webencodings==0.5.1

View File

@ -2,25 +2,26 @@
-r requirements.txt
appdirs==1.4.3
bonobo-sqlalchemy==0.6.0
certifi==2018.4.16
cached-property==1.5.1
certifi==2019.3.9
chardet==3.0.4
colorama==0.3.9
fs==2.0.27
fs==2.4.5
graphviz==0.8.4
idna==2.7
jinja2==2.10
markupsafe==1.0
idna==2.8
jinja2==2.10.1
markupsafe==1.1.1
mondrian==0.8.0
packaging==17.1
pbr==4.2.0
psutil==5.4.6
pyparsing==2.2.0
python-slugify==1.2.5
pytz==2018.5
requests==2.19.1
six==1.11.0
sqlalchemy==1.2.10
stevedore==1.29.0
unidecode==1.0.22
urllib3==1.23
pbr==5.2.0
psutil==5.6.2
pyparsing==2.4.0
python-slugify==1.2.6
pytz==2019.1
requests==2.21.0
six==1.12.0
sqlalchemy==1.3.3
stevedore==1.30.1
unidecode==1.0.23
urllib3==1.24.3
whistle==1.0.1

View File

@ -1,24 +1,24 @@
-e .
appdirs==1.4.3
cached-property==1.4.3
certifi==2018.4.16
cached-property==1.5.1
certifi==2019.3.9
chardet==3.0.4
colorama==0.3.9
fs==2.0.27
fs==2.4.5
graphviz==0.8.4
idna==2.7
jinja2==2.10
markupsafe==1.0
idna==2.8
jinja2==2.10.1
markupsafe==1.1.1
mondrian==0.8.0
packaging==17.1
pbr==4.2.0
psutil==5.4.6
pyparsing==2.2.0
python-slugify==1.2.5
pytz==2018.5
requests==2.19.1
six==1.11.0
stevedore==1.29.0
unidecode==1.0.22
urllib3==1.23
packaging==19.0
pbr==5.2.0
psutil==5.6.2
pyparsing==2.4.0
python-slugify==1.2.6
pytz==2019.1
requests==2.21.0
six==1.12.0
stevedore==1.30.1
unidecode==1.0.23
urllib3==1.24.3
whistle==1.0.1

107
setup.py
View File

@ -1,12 +1,11 @@
# Generated by Medikit 0.6.3 on 2018-08-11.
# Generated by Medikit 0.7.1 on 2019-05-16.
# All changes will be overriden.
# Edit Projectfile and run “make update” (or “medikit update”) to regenerate.
from setuptools import setup, find_packages
from codecs import open
from os import path
from setuptools import find_packages, setup
here = path.abspath(path.dirname(__file__))
# Py3 compatibility hacks, borrowed from IPython.
@ -21,88 +20,76 @@ except NameError:
# Get the long description from the README file
try:
with open(path.join(here, "README.rst"), encoding="utf-8") as f:
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = f.read()
except:
long_description = ""
long_description = ''
# Get the classifiers from the classifiers file
tolines = lambda c: list(filter(None, map(lambda s: s.strip(), c.split("\n"))))
tolines = lambda c: list(filter(None, map(lambda s: s.strip(), c.split('\n'))))
try:
with open(path.join(here, "classifiers.txt"), encoding="utf-8") as f:
with open(path.join(here, 'classifiers.txt'), encoding='utf-8') as f:
classifiers = tolines(f.read())
except:
classifiers = []
version_ns = {}
try:
execfile(path.join(here, "bonobo/_version.py"), version_ns)
execfile(path.join(here, 'bonobo/_version.py'), version_ns)
except EnvironmentError:
version = "dev"
version = 'dev'
else:
version = version_ns.get("__version__", "dev")
version = version_ns.get('__version__', 'dev')
setup(
author="Romain Dorgueil",
author_email="romain@dorgueil.net",
data_files=[
(
"share/jupyter/nbextensions/bonobo-jupyter",
[
"bonobo/contrib/jupyter/static/extension.js",
"bonobo/contrib/jupyter/static/index.js",
"bonobo/contrib/jupyter/static/index.js.map",
],
)
],
description=("Bonobo, a simple, modern and atomic extract-transform-load toolkit for " "python 3.5+."),
license="Apache License, Version 2.0",
name="bonobo",
python_requires=">=3.5",
author='Romain Dorgueil',
author_email='romain@dorgueil.net',
data_files=[('share/jupyter/nbextensions/bonobo-jupyter', [
'bonobo/contrib/jupyter/static/extension.js',
'bonobo/contrib/jupyter/static/index.js',
'bonobo/contrib/jupyter/static/index.js.map'
])],
description=(
'Bonobo, a simple, modern and atomic extract-transform-load toolkit for '
'python 3.5+.'),
license='Apache License, Version 2.0',
name='bonobo',
python_requires='>=3.5',
version=version,
long_description=long_description,
classifiers=classifiers,
packages=find_packages(exclude=["ez_setup", "example", "test"]),
packages=find_packages(exclude=['ez_setup', 'example', 'test']),
include_package_data=True,
install_requires=[
"cached-property (~= 1.4)",
"fs (~= 2.0)",
"graphviz (>= 0.8, < 0.9)",
"jinja2 (~= 2.9)",
"mondrian (~= 0.8)",
"packaging (~= 17.0)",
"psutil (~= 5.4)",
"python-slugify (~= 1.2.0)",
"requests (~= 2.0)",
"stevedore (~= 1.27)",
"whistle (~= 1.0)",
'cached-property (~= 1.4)', 'fs (~= 2.0)', 'graphviz (>= 0.8, < 0.9)',
'jinja2 (~= 2.9)', 'mondrian (~= 0.8)', 'packaging (~= 19.0)',
'psutil (~= 5.4)', 'python-slugify (~= 1.2.0)', 'requests (~= 2.0)',
'stevedore (~= 1.27)', 'whistle (~= 1.0)'
],
extras_require={
"dev": [
"cookiecutter (>= 1.5, < 1.6)",
"coverage (~= 4.4)",
"pytest (~= 3.4)",
"pytest-cov (~= 2.5)",
"pytest-timeout (>= 1, < 2)",
"sphinx (~= 1.7)",
"sphinx-sitemap (>= 0.2, < 0.3)",
'dev': [
'cookiecutter (>= 1.5, < 1.6)', 'coverage (~= 4.4)',
'pytest (~= 3.4)', 'pytest-cov (~= 2.5)',
'pytest-timeout (>= 1, < 2)', 'sphinx (~= 1.7)',
'sphinx-sitemap (>= 0.2, < 0.3)'
],
"docker": ["bonobo-docker (~= 0.6.0a1)"],
"jupyter": ["ipywidgets (~= 6.0)", "jupyter (~= 1.0)"],
"sqlalchemy": ["bonobo-sqlalchemy (~= 0.6.0a1)"],
'docker': ['bonobo-docker (~= 0.6.0a1)'],
'jupyter': ['ipywidgets (~= 6.0)', 'jupyter (~= 1.0)'],
'sqlalchemy': ['bonobo-sqlalchemy (~= 0.6.0a1)']
},
entry_points={
"bonobo.commands": [
"convert = bonobo.commands.convert:ConvertCommand",
"download = bonobo.commands.download:DownloadCommand",
"examples = bonobo.commands.examples:ExamplesCommand",
"init = bonobo.commands.init:InitCommand",
"inspect = bonobo.commands.inspect:InspectCommand",
"run = bonobo.commands.run:RunCommand",
"version = bonobo.commands.version:VersionCommand",
'bonobo.commands': [
'convert = bonobo.commands.convert:ConvertCommand',
'download = bonobo.commands.download:DownloadCommand',
'examples = bonobo.commands.examples:ExamplesCommand',
'init = bonobo.commands.init:InitCommand',
'inspect = bonobo.commands.inspect:InspectCommand',
'run = bonobo.commands.run:RunCommand',
'version = bonobo.commands.version:VersionCommand'
],
"console_scripts": ["bonobo = bonobo.commands:entrypoint"],
'console_scripts': ['bonobo = bonobo.commands:entrypoint']
},
url="https://www.bonobo-project.org/",
download_url="https://github.com/python-bonobo/bonobo/tarball/{version}".format(version=version),
url='https://www.bonobo-project.org/',
download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.
format(version=version),
)

View File

@ -117,4 +117,4 @@ class CsvWriterTest(Csv, WriterTest, TestCase):
context.write_sync(EMPTY, EMPTY, EMPTY)
context.stop()
assert self.readlines() == ('', '', '')
assert self.readlines() == ("", "", "")

View File

@ -119,23 +119,26 @@ def test_methodcaller():
MyBag = BagType("MyBag", ["a", "b", "c"])
@pytest.mark.parametrize("input_, key, expected", [
(MyBag(1, 2, 3), True, MyBag(1, 4, 9)),
(MyBag(1, 2, 3), False, MyBag(1, 2, 3)),
(MyBag(1, 2, 3), lambda x: x == 'c', MyBag(1, 2, 9)),
((1, 2, 3), True, (1, 4, 9)),
((1, 2, 3), False, (1, 2, 3)),
])
@pytest.mark.parametrize(
"input_, key, expected",
[
(MyBag(1, 2, 3), True, MyBag(1, 4, 9)),
(MyBag(1, 2, 3), False, MyBag(1, 2, 3)),
(MyBag(1, 2, 3), lambda x: x == "c", MyBag(1, 2, 9)),
((1, 2, 3), True, (1, 4, 9)),
((1, 2, 3), False, (1, 2, 3)),
],
)
def test_map_fields(input_, key, expected):
with BufferingNodeExecutionContext(bonobo.MapFields(lambda x: x**2, key)) as context:
with BufferingNodeExecutionContext(bonobo.MapFields(lambda x: x ** 2, key)) as context:
context.write_sync(input_)
assert context.status == '-'
assert context.status == "-"
[got] = context.get_buffer()
assert expected == got
def test_map_fields_error():
with BufferingNodeExecutionContext(bonobo.MapFields(lambda x: x**2, lambda x: x == 'c')) as context:
with BufferingNodeExecutionContext(bonobo.MapFields(lambda x: x ** 2, lambda x: x == "c")) as context:
context.write_sync(tuple())
assert context.status == '!'
assert context.status == "!"
assert context.defunct

View File

@ -1,7 +1,7 @@
import pytest
from bonobo.util import ensure_tuple, sortedlist
from bonobo.util.collections import cast, tuplize, tuple_or_const
from bonobo.util.collections import cast, tuple_or_const, tuplize
def test_sortedlist():
@ -15,10 +15,11 @@ def test_sortedlist():
def test_tuple_or_const():
assert tuple_or_const(()) == ()
assert tuple_or_const((1, )) == (1, )
assert tuple_or_const((1, 2, )) == (1, 2, )
assert tuple_or_const([1, 2, ]) == (1, 2, )
assert tuple_or_const("aaa") == ('aaa', )
assert tuple_or_const((1,)) == (1,)
assert tuple_or_const((1, 2)) == (1, 2)
assert tuple_or_const([1, 2]) == (1, 2)
assert tuple_or_const("aaa") == ("aaa",)
def test_ensure_tuple():
assert ensure_tuple("a") == ("a",)