diff --git a/doc/_static/idioms/adapter.py b/doc/_static/idioms/adapter.py new file mode 100644 index 00000000..00d62002 --- /dev/null +++ b/doc/_static/idioms/adapter.py @@ -0,0 +1,15 @@ +# With Adapter +for first, item in loop_first([]): + if first: + edge_case(item) + continue + default(item) + +# Naive Approach +is_first = True +for items in []: + if is_first: + edge_case(item) + is_first = False + continue + default(item) diff --git a/doc/_static/idioms/concat.py b/doc/_static/idioms/concat.py new file mode 100644 index 00000000..dfc58e32 --- /dev/null +++ b/doc/_static/idioms/concat.py @@ -0,0 +1,11 @@ +# Good +names = ["Peter", "Albert", "Cleo", "Ember"] +output = ", ".join(names) + +# Bad +names = ["Peter", "Albert", "Cleo", "Ember"] +output = "" +for name in names: + output += name + if not is_last_element(name): + output += ", " diff --git a/doc/_static/idioms/context_decorator.py b/doc/_static/idioms/context_decorator.py new file mode 100644 index 00000000..7bb99ea8 --- /dev/null +++ b/doc/_static/idioms/context_decorator.py @@ -0,0 +1,42 @@ +# With Decorator +import logging +from functools import wraps + + +def log_execution(f): + @wraps(f) + def wrapper(*args, **kwargs): + logging.debug(f"Entering: {f.__name__}") + result = f(*args, **kwargs) + logging.debug(f"Leaving: {f.__name__}") + return result + + return wrapper + + +@log_execution +def some_function(): + return "Some Result" + + +@log_execution +def other_function(): + return "Other Result" + + +# Naive Approach +import logging + + +def some_function(): + logging.debug("Entering: some_function") + result = "Some Result" + logging.debug("Leaving: some_function") + return result + + +def other_function(): + logging.debug("Entering: other_function") + result = "Some Result" + logging.debug("Leaving: other_function") + return result diff --git a/doc/_static/idioms/context_filter.py b/doc/_static/idioms/context_filter.py new file mode 100644 index 00000000..e69de29b diff --git a/doc/_static/idioms/context_manager.py b/doc/_static/idioms/context_manager.py new file mode 100644 index 00000000..b56d6dd8 --- /dev/null +++ b/doc/_static/idioms/context_manager.py @@ -0,0 +1,40 @@ +# With Context Manager +import os +from contextlib import contextmanager + + +@contextmanager +def chdir(path): + old_dir = os.getcwd() + os.chdir(path) + yield path + os.chdir(old_dir) + + +def initialize(directory): + with chdir(directory) as _working_dir: + with open('some-file.txt', 'w') as f: + f.write("Some content") + + +# With Python 3.11 +from contextlib import chdir + + +def initialize(directory): + with chdir(directory) as _working_dir: + with open('some-file.txt', 'w') as f: + f.write("Some content") + + +# Naive Approach +import os + + +def initialize(directory): + old_dir = os.getcwd() + os.chdir(directory) + os.chdir(old_dir) + with open('some-file.txt', 'w') as f: + f.write("Some content") + os.chdir(old_dir) diff --git a/doc/_static/idioms/count.py b/doc/_static/idioms/count.py new file mode 100644 index 00000000..de1bbdcd --- /dev/null +++ b/doc/_static/idioms/count.py @@ -0,0 +1,15 @@ +# Good +from collections import Counter + +colors = ["green", "blue", "red", "green", "red", "red"] + +counts = Counter(colors) + +# Bad +from collections import defaultdict + +colors = ["green", "blue", "red", "green", "red", "red"] + +d = defaultdict(int) +for color in colors: + d[color] += 1 diff --git a/doc/_static/idioms/enumerate.py b/doc/_static/idioms/enumerate.py new file mode 100644 index 00000000..ce16bce0 --- /dev/null +++ b/doc/_static/idioms/enumerate.py @@ -0,0 +1,11 @@ +# Good +customers = ["Marry", "Thor", "Peter", "Batman"] +for index, customer in enumerate(customers): + print(f"Customer: {customer}, Queue position: {index}") + +# Bad +index = 0 +customers = ["Marry", "Thor", "Peter", "Batman"] +for customer in customers: + print(f"Customer: {customer}, Queue position: {index}") + index += 1 diff --git a/doc/_static/idioms/filters.py b/doc/_static/idioms/filters.py new file mode 100644 index 00000000..f3fb1fa5 --- /dev/null +++ b/doc/_static/idioms/filters.py @@ -0,0 +1,21 @@ +# With Filter +numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] +odd = filter(lambda n: n % 2, numbers) +even = (number for number in numbers if not number % 2) + +print(f"Sum of odd values: {sum(odd)}") +print(f"Sum of even values: {sum(even)}") + +# Naive Approach +numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] +odd = 0 +even = 0 + +for number in numbers: + if number % 2: + odd += number + else: + even += number + +print(f"Sum of odd values: {odd}") +print(f"Sum of even values: {even}") diff --git a/doc/_static/idioms/format.py b/doc/_static/idioms/format.py new file mode 100644 index 00000000..deda125c --- /dev/null +++ b/doc/_static/idioms/format.py @@ -0,0 +1,26 @@ +# Good +template = """ +{heading} - from: {date} +--------------------------------- +{users} +""" +output = template.format( + heading="User Overview", + date=datetime.now().strftime("%Y-%m-%d"), + users="\n".join( + ( + f"Firstname: {user.first_name}, Lastname: {user.last_name}, Age: {user.age}" + for user in users + ) + ), +) + +# Bad +heading = "User Overview" +date = datetime.now().strftime("%Y-%m-%d") +output = f"{heading} - from: {date}" + "\n" +output += "---------------------------------" + "\n" +for user in users: + output += ( + f"Firstname: {user.first_name}, Lastname: {user.last_name}, Age: {user.age}\n" + ) diff --git a/doc/_static/idioms/fstring.py b/doc/_static/idioms/fstring.py new file mode 100644 index 00000000..7df979d2 --- /dev/null +++ b/doc/_static/idioms/fstring.py @@ -0,0 +1,7 @@ +# Good +output = f"Firstname: {user.first_name}, Lastname: {user.last_name}, Age: {user.age}" + +# Bad +output = "Firstname: {}, Lastname: {}, Age: {}".format( + user.first_name, user.last_name, user.age +) diff --git a/doc/_static/idioms/layering.py b/doc/_static/idioms/layering.py new file mode 100644 index 00000000..abca190a --- /dev/null +++ b/doc/_static/idioms/layering.py @@ -0,0 +1,18 @@ +# Good +from collections import ChainMap + +cli_args = {"user": "FooBar", "cmd": "ls -all"} +env_args = {"user": "Johndoe"} +cfg_args = {"user": "default", "cwd": "/home/default", "cmd": "ls"} + +config = ChainMap(cli_args, env_args, cfg_args) + +# Bad +cli_args = {"user": "FooBar", "cmd": "ls -all"} +env_args = {"user": "Johndoe"} +cfg_args = {"user": "default", "cwd": "/home/default", "cmd": "ls"} + +config = {} +config.update(cfg_args) +config.update(env_args) +config.update(cli_args) diff --git a/doc/_static/idioms/loop_collection.py b/doc/_static/idioms/loop_collection.py new file mode 100644 index 00000000..ce7afa96 --- /dev/null +++ b/doc/_static/idioms/loop_collection.py @@ -0,0 +1,9 @@ +# Good +colors = ["red", "green", "blue", "yellow"] +for color in colors: + print(color) + +# Bad +colors = ["red", "green", "blue", "yellow"] +for i in range(len(colors)): + print(colors[i]) diff --git a/doc/_static/idioms/loop_numbers.py b/doc/_static/idioms/loop_numbers.py new file mode 100644 index 00000000..ce6aef12 --- /dev/null +++ b/doc/_static/idioms/loop_numbers.py @@ -0,0 +1,7 @@ +# Good +for i in range(6): + print(i**2) + +# Bad +for i in [0, 1, 2, 3, 4, 5]: + print(i**2) diff --git a/doc/_static/idioms/loop_two_collections.py b/doc/_static/idioms/loop_two_collections.py new file mode 100644 index 00000000..b31e5e1b --- /dev/null +++ b/doc/_static/idioms/loop_two_collections.py @@ -0,0 +1,14 @@ +# Good +names = ["raymond", "rachel", "matthew"] +colors = ["red", "green", "blue", "yellow"] + +for name, color in zip(names, colors): + print(f"{name} --> {color}") + +# Bad +names = ["raymond", "rachel", "matthew"] +colors = ["red", "green", "blue", "yellow"] + +n = min(len(names), len(colors)) +for i in range(n): + print(f"{names[i]} --> {colors[i]}") diff --git a/doc/_static/idioms/named_parameters.py b/doc/_static/idioms/named_parameters.py new file mode 100644 index 00000000..49769b50 --- /dev/null +++ b/doc/_static/idioms/named_parameters.py @@ -0,0 +1,4 @@ +# Good +twitter_search("@obama", retweets=False, numtweets=20, popular=True) +# Bad +twitter_search("@obama", False, 20, True) diff --git a/doc/_static/idioms/pairs.py b/doc/_static/idioms/pairs.py new file mode 100644 index 00000000..c8ff514e --- /dev/null +++ b/doc/_static/idioms/pairs.py @@ -0,0 +1,9 @@ +# Good +colors = ["red", "green", "blue"] +d = dict(enumerate(colors)) + +# Bad +colors = ["red", "green", "blue"] +d = {} +for i, color in enumerate(colors): + d[i] = color diff --git a/doc/_static/idioms/reverse.py b/doc/_static/idioms/reverse.py new file mode 100644 index 00000000..6de360bb --- /dev/null +++ b/doc/_static/idioms/reverse.py @@ -0,0 +1,9 @@ +# Good +customers = ["Marry", "Brain", "Peter", "Batman"] +for customer in reversed(customers): + print(customer) + +# Bad +customers = ["Marry", "Brain", "Peter", "Batman"] +for index in range(len(customers) - 1, -1, -1): + print(customers[index]) diff --git a/doc/_static/idioms/sentinel.py b/doc/_static/idioms/sentinel.py new file mode 100644 index 00000000..8c99a7d4 --- /dev/null +++ b/doc/_static/idioms/sentinel.py @@ -0,0 +1,12 @@ +# Good +blocks = [] +for block in iter(partial(f.read, 32), ""): + blocks.append(block) + +# Bad +blocks = [] +while True: + block = f.read(32) + if block == "": + break + blocks.append(block) diff --git a/doc/_static/idioms/short_circuit.py b/doc/_static/idioms/short_circuit.py new file mode 100644 index 00000000..02a4fd65 --- /dev/null +++ b/doc/_static/idioms/short_circuit.py @@ -0,0 +1,28 @@ +# With Generator Short Circuit +numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] + + +def find_number(numbers): + numbers = (n for n in numbers if (n % 3) == 0) + try: + number = next(numbers) + except StopIteration: + number = None + return number + + +number = find_number(numbers) + +# Naive Approach +numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] + + +def find_number(numbers): + for n in numbers: + # concerns are not seperated + if (n % 3) == 0: + return n + return None + + +number = find_number(numbers) diff --git a/doc/_static/idioms/unpacking.py b/doc/_static/idioms/unpacking.py new file mode 100644 index 00000000..f7888978 --- /dev/null +++ b/doc/_static/idioms/unpacking.py @@ -0,0 +1,11 @@ +# Good +person = "John", "Doe", 42, "john.doe@example.net" + +firstname, lastname, age, email = person +# Bad +person = "John", "Doe", 42, "john.doe@example.net" + +firstname = person[0] +lastname = person[1] +age = person[2] +email = person[3] diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 63d18b24..9ead303d 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -19,4 +19,6 @@ * Updated actions/checkout to v4 ## 📚 Documentation +* Add Python Styleguide +* Add Issue Guide * Updated User Documentation diff --git a/doc/conf.py b/doc/conf.py index e874f372..e4e85571 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -36,6 +36,8 @@ "sphinx_copybutton", "myst_parser", "sphinx_design", + "sphinx_inline_tabs", + "sphinx_copybutton", ] intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} diff --git a/doc/index.rst b/doc/index.rst index 9c57c5b6..b9b5bded 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,6 +19,18 @@ Documentation of the Exasol-Toolbox Instructions and best practices to help developers contribute to the project and set up their development environment. + .. grid-item-card:: :octicon:`paintbrush` Python Styleguide + :link: styleguide + :link-type: ref + + Python Style Guide of the Exasol Integration Team + + .. grid-item-card:: :octicon:`list-unordered` Issue Tracking Guide + :link: issue_tracking + :link-type: ref + + Guide to Issues and Issue Tracking + .. grid-item-card:: :octicon:`terminal` Tools :link: tools :link-type: ref @@ -51,11 +63,13 @@ Documentation of the Exasol-Toolbox .. toctree:: - :maxdepth: 1 + :maxdepth: 4 :hidden: user_guide/user_guide developer_guide/developer_guide + styleguide/index + issues tools github_actions/github_actions api diff --git a/doc/issues.rst b/doc/issues.rst new file mode 100644 index 00000000..24684896 --- /dev/null +++ b/doc/issues.rst @@ -0,0 +1,269 @@ +.. _issue_tracking: + +:octicon:`list-unordered` Issue Tracking Guide +============================================== + +Summary ++++++++ +Issue tracking is a great tool to track pending work and improve the overall communication within a project. +But similar to every other tool, it needs to be used with care. + + | "A fool with a tool is still a fool" + | -- Grady Booch + +This guide will try to help you to get the best out of the issue tracking for you, your teammates and the project(s) +you are working on, by answering the following questions: + +* When to create an issue? +* What makes a good issue? +* How to maintain an issue? + +When to create an issue? +++++++++++++++++++++++++ +Let me start by saying there is no perfect and one size fits all ruleset to decide this question, otherwise there would be some code doing that for us. +The good thing though is, humans are great in dealing with fuzzy problems. Still most people want to have something to orient themselves. +Asking more specific questions often helps in this situation. + +The following question(s), helped us in the past, to get a better handle on this: + +* What is the cost/benefit ratio of creating an issue? + * cost being mainly time and effort + * benefit being the upsides we gain from having an issue + +* What is the signal to noise ratio? + * signal being the actual code/change bringing some benefit/value to the project + * noises being the overhead + +* Is the user (api or product) impacted by this change? + * Is it an "internal improvement" quality of life and/or code quality + + +Examples +-------- +Fixing a couple of typos +________________________ +You stumble upon a couple of typos in a Project. + +Possible Action(s) +~~~~~~~~~~~~~~~~~~ +* You fix the typos without creating an issue + +Suspicious amount of typos +___________________________ +You stumble upon a various typos within a piece of documentation you are reading. Because of the vast amount +of typos and the overall structure you expect the whole documentation to contain a substantial amount of typos. + +Possible Action(s) +~~~~~~~~~~~~~~~~~~ +* If the documentation is not to big and you can make the time to fix the typos right away without creating an issue. +* If the documentation is quite big, and fixing this e.g. would take you more than 1/2 a day you definitely should create an issue. +* You are unsure how long it will take to address and check the documentation and you currently can't spent time for detours create an issue. + +What makes a good issue ++++++++++++++++++++++++ + + **TL;DR:** An good issue describes a piece of work, it's context and it's assumption in a way, any person capable of working + on the project, can understand it, based on the information referenced and within the issue. + +To begin with, we want to make sure you understand that creating good a issue is not a zero effort action. Various things +like issue templates, can remove tedious boilerplate work, but still creating a good issue is some actual work. +It is crucial to have well crafted issues, because bad issues do way more harm than good. So let's take a step +back and let's think about why we need to have good issues, in order to better understand what actually makes a good issue. + +Boiled down to it's core issue tracking is just a list of pieces of work which are needed to be done. +This list in turn, then can be used to plan, organize and track "progress/work". + +Broken down to an individual issue, this means an issue should provide all relevant information about +a "single" piece/unit of work. + +Even though the part what an issue is about is well understood, by most people, this is also where it goes wrong. + +**Why?** Because we as humans tend to use and assume context implicitly. Assuming context implicitly already can be a problem +when communicating with people we meet on a regular basis, but is a huge problem when you communicate with people which +we meet rarely or even never. + +This is somewhat obvious when you think about the fact that 60-70 % of human communication is non verbal communication. +This most likely also part of the reason so many people use emojies, a picture says more than a 1000 words +and therefore can help transport context more easily 😉. But even those helpers aren't perfect and it very +often depends on the mood and the other persons perception (context) of you how they interpret a specific emoji you have sent. + +To take on a more computer science specific example: + +.. list-table:: **TL;DR: Context matters** + :header-rows: 1 + + * - Information + - Context + - Value + * - 65 + - ASCII + - 'A' + * - 65 + - Hex + - 101 + * - 65 + - Decimal + - 65 + + +As you may have realized by know our point being how information is understood and processed highly depends on the context. + +To come back now to the topic of **"What makes a good issue"**, a major part of it is to make context and assumptions +an explicit part of your information you are providing. Write the issue in such a way, that a person +working on the project, now or in the future can precisely understand the task, the context and assumptions +of it. + + +.. attention:: + + You may inclined to think, you gonna address this task anyway or it is just for "your" book keeping, + but then this is nothing for the issue tracker rather something for your personal todo list. + If you decide it is important enough for the issue tracker than treat it as such. + + +**So what does this mean in more practical actionable terms?** + +* Make your context explicit + - Add links and references to spec you may already know + - Add information e.g. from discussion, meetings, mails ... + +* Make your assumptions explicit + - Write them down also note e.g. if you are not sure if it is the right decision but what you have taken into account at the point in time when you wrote it down + +* Add SubTasks regarding "standard" processes which people may not necessarily know about + - This can be simplified e.g. by providing issue templates + - update documentation + - update changelog + - ... + +* Make it easy to answer the following questions for the person working/reading the issue + - Is this issue still relevant, or is it obsolete by now? + - Are the assumptions still valid today? + - Did they have more or less context than we have today? + - Did I consider the assumptions and context of the one writing the issue? + + +.. attention:: + + More details on a specific issue type you will find in the corresponding subsection(s) of this guide. + + +How to maintain an issue +++++++++++++++++++++++++ +As you already know an issue does not only keep track of what needs to be done, but also about it's context (how, why, etc.). +This context can be a quite dynamic though, especially for tasks which bare a larger amount of uncertainty (e.g. bugs). +So a huge part of keeping an issue up to date, is keep updating it's context. The following scenarios will +try to give you an idea on how to update and maintain an issue in various scenarios. + +TL;DR +----- + +* Keep the issue and it's context up to date + * Keep the status up to date [Backlog, In Progress, ...] + * Update tickets which are actively worked on regularly (at least every 2-3 days) + * Try to communicate new information via comments (e.g. status update's) + * Report important changes because of internal and external events + +* Comments + * Respond to comments in a timely manner + * Stay friendly and focused on the issue when responding to comments + + + +Scenarios +--------- + +A developer picks up the issue from the backlog and starts working on it +________________________________________________________________________ + +* Assign the ticket to the developer working on it +* Change the state of the issue to "In Progress" + + .. note:: + + Most issue tracking system already take care of this if you move it + to the appropriate category in the issue(s) overview. + +The Issue Receives a comment +____________________________ + +* Make sure to reply to a comment in a timely manner +* If it is the first contribution/comment of the contributor on this issue, show appreciation for the contribution. + * If the comment can't be addressed right away, at least acknowledge the comment and communicate some timeframe +* Make sure the comment can be understood in the context of the issue + * If needed ask for clarification + * If needed ask follow up questions +* Be clear in your response(s) + * Make sure relevance, importance, course of action and timeframe are communicated if necessary + +The Developer stop's/postpones working on the issue, e.g. due to re prioritization +__________________________________________________________________________________ + +* Remove assignee on the ticket +* Change the state of the issue to "Backlog" + + .. note:: + + Most issue tracking system already take care of this if you move it + to the appropriate category in the issue(s) overview. + +* Make sure all current context information is updated or added as status comment to the issue +* Add information about why working on the issue have been postponed/stopped (e.g. blocked) + * Except if this would disclose information to the wrong audience +* Communicate when work on the issue will be picked up again + * Communicating that it is unclear when the work will be continued, is also fine + +Stalled issue (e.g. root cause of bug is hard to find) +______________________________________________________ + +.. note:: + + Very rarely the issue (work) is really stalled, often it just feels like this. + When you can't report on explicitly achieved task(s), provide context information + about your work (journey). This can help you and others to pickup on it latter + or at least it help to understand the current state more clearly. + +* Regularly post a status update on the issue (every 2-3 days) + * What issues/problems already have been ruled out and why? + * What is the current strategy to find the culprit? + * What is the current strategy to resolve roadblocks? + * What are the current assumptions and hunches? + * Are there any further leads to be investigated in the future? + * Describe the current roadblocks + * Report sub partial successes + * Add script's and context information which helps to reproduce and/or trigger the bug + + +**Example:** Status Update on Bug Issue + +.. code-block:: markdown + + # Status Update + Further investigation's have shown that the basic SQLA test suite mostly is intact after the upgrade to `1.4` (14 failures), when run in "isolation". Various exasol specific test suites: + + * test/test_get_metadata_functions.py + * test/test_large_metadata.py + * test/test_regression.py (TranslateMap) + + seem to have negative side effects which cause 100+ tests to :boom: fail/crash, if run after those test suites. + This further strengthens the case for the **assumptions** mentioned in the previous update: + + * Setup/Teardown mechanic of `testing.fixtures.TablesTest` has changed + * Setup/Teardown mechanic fails due to leftovers in tests DB + + Also, this narrows down the potential root cause(s). + + ## Remark(s) + Common to all those test suites to be that they add/remove schemas. + For `test/test_regression.py` it have been proven that the schema manipulating test (`TranslateMap`) causes some negative side effect on following test suits. + + ## Notes & Ideas (from discussion with @tkilias ) + * Is schema inference still working correctly? + * Does the "disabled" caching cause side effects? + * Do implicit schema open and closes affect the current schema for follow up tests? + + ## Next Steps + * Analyze effects of implicit open/close of schema(s) + * A more in depth analysis regarding side effects and cleanup of the mentioned test suites will be done + +To see the example update in it's full context look `here `_. diff --git a/doc/styleguide/guides/idioms/dictionaries.rst b/doc/styleguide/guides/idioms/dictionaries.rst new file mode 100644 index 00000000..a9c64369 --- /dev/null +++ b/doc/styleguide/guides/idioms/dictionaries.rst @@ -0,0 +1,80 @@ +Dictionaries +------------ + +From Pairs +++++++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/pairs.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/pairs.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/pairs.py + :language: python3 + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + +Count ++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/count.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/count.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/count.py + :language: python3 + +Layering +++++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/layering.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/layering.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/layering.py + :language: python3 + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + + +.. _Raymond Hettinger: https://github.com/rhettinger +.. _Transform Code into Beautiful, Idiomatic Python: https://www.youtube.com/watch?v=OSGv2VnC0go> +.. _Transform Python Slides: https://speakerdeck.com/pyconslides/transforming-code-into-beautiful-idiomatic-python-by-raymond-hettinger-1 diff --git a/doc/styleguide/guides/idioms/general.rst b/doc/styleguide/guides/idioms/general.rst new file mode 100644 index 00000000..343f7515 --- /dev/null +++ b/doc/styleguide/guides/idioms/general.rst @@ -0,0 +1,64 @@ +General +------- + +Named Parameters +++++++++++++++++ +Use named parameters for inline primitive types (when it makes the call site easier to understand). + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/named_parameters.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/named_parameters.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/named_parameters.py + :language: python3 + +.. note:: + + Consider using `keyword only arguments `_ when defining API's. + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + +Unpacking ++++++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/unpacking.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/unpacking.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/unpacking.py + :language: python3 + + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + +.. _Raymond Hettinger: https://github.com/rhettinger +.. _Transform Code into Beautiful, Idiomatic Python: https://www.youtube.com/watch?v=OSGv2VnC0go> +.. _Transform Python Slides: https://speakerdeck.com/pyconslides/transforming-code-into-beautiful-idiomatic-python-by-raymond-hettinger-1 diff --git a/doc/styleguide/guides/idioms/idioms.rst b/doc/styleguide/guides/idioms/idioms.rst new file mode 100644 index 00000000..6bf93f21 --- /dev/null +++ b/doc/styleguide/guides/idioms/idioms.rst @@ -0,0 +1,41 @@ +Idioms +------ + +The initial list of this idioms have been taken form `Raymond Hettinger`_'s +Talk - "`Transform Code into Beautiful, Idiomatic Python`_" (`Transform Python Slides`_). +The list then been updated and extended, we will do our best to give credit for each idom to the source +where we picked it up from, by annotating it the following way: + + 💡 learnt from: , [Reference] + + or + + 💡 learnt from: + + * Source: + * Reference: [Reference] + + --- + + : being an author name, book name, etc. + + [Reference]: Optional, a concrete reference to the idiom or the source where it stems from. + +That being said, one should be aware that small patterns and idioms often times get "invented" independently +by multiple sources. It is not always clear who was the original source of a specific pattern or idiom. +Therefore our marker intentionally was named *learnt from* because we want communicate, it is the source +where we picked it up from, rather than the "original" source. + +.. toctree:: + :maxdepth: 1 + + general + strings + looping + dictionaries + seperation_of_concerns + + +.. _Raymond Hettinger: https://github.com/rhettinger +.. _Transform Code into Beautiful, Idiomatic Python: https://www.youtube.com/watch?v=OSGv2VnC0go> +.. _Transform Python Slides: https://speakerdeck.com/pyconslides/transforming-code-into-beautiful-idiomatic-python-by-raymond-hettinger-1 diff --git a/doc/styleguide/guides/idioms/looping.rst b/doc/styleguide/guides/idioms/looping.rst new file mode 100644 index 00000000..35655733 --- /dev/null +++ b/doc/styleguide/guides/idioms/looping.rst @@ -0,0 +1,170 @@ +Looping +------- + +Collection +++++++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/loop_collection.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/loop_collection.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/loop_collection.py + :language: python3 + + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + + +Numbers +++++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/loop_numbers.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/loop_numbers.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/loop_numbers.py + :language: python3 + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + +Enumerate ++++++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/enumerate.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/enumerate.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/enumerate.py + :language: python3 + + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + + +Reverse ++++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/reverse.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/reverse.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/reverse.py + :language: python3 + + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + +Two Collections ++++++++++++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/loop_two_collections.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/loop_two_collections.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/loop_two_collections.py + :language: python3 + + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + + +Sentinel +++++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/sentinel.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/sentinel.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/sentinel.py + :language: python3 + + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + +.. _Raymond Hettinger: https://github.com/rhettinger +.. _Transform Code into Beautiful, Idiomatic Python: https://www.youtube.com/watch?v=OSGv2VnC0go> +.. _Transform Python Slides: https://speakerdeck.com/pyconslides/transforming-code-into-beautiful-idiomatic-python-by-raymond-hettinger-1 diff --git a/doc/styleguide/guides/idioms/seperation_of_concerns.rst b/doc/styleguide/guides/idioms/seperation_of_concerns.rst new file mode 100644 index 00000000..3db88ad4 --- /dev/null +++ b/doc/styleguide/guides/idioms/seperation_of_concerns.rst @@ -0,0 +1,273 @@ +Separation of Concerns +---------------------- + +Filters ++++++++ + +.. tab:: ✅ With Filter + + .. literalinclude:: ../../../_static/idioms/filters.py + :language: python3 + :start-after: # With Filter + :end-before: # Naive Approach + +.. tab:: ❌ Naive Approach + + .. literalinclude:: ../../../_static/idioms/filters.py + :language: python3 + :start-after: # Naive Approach + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/filters.py + :language: python3 + + +Adapters +++++++++ + +.. tab:: ✅ With Adapter + + .. literalinclude:: ../../../_static/idioms/adapter.py + :language: python3 + :start-after: # With Adapter + :end-before: # Naive Approach + +.. tab:: ❌ Naive Approach + + .. literalinclude:: ../../../_static/idioms/adapter.py + :language: python3 + :start-after: # Naive Approach + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/adapter.py + :language: python3 + + +**💡 learnt from:** + +* Source: `Will McGugan`_ +* Reference: `Stealing Open Source code from Textual`_ + + `Adapter's used in textual `_ + + .. tab:: loop_first + + .. code-block:: python + + from __future__ import annotations + + from typing import Iterable, TypeVar + + T = TypeVar("T") + + + def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]: + """Iterate and generate a tuple with a flag for first value.""" + iter_values = iter(values) + try: + value = next(iter_values) + except StopIteration: + return + yield True, value + for value in iter_values: + yield False, value + + + .. tab:: loop_last + + .. code-block:: python + + from __future__ import annotations + + from typing import Iterable, TypeVar + + T = TypeVar("T") + + + def loop_last(values: Iterable[T]) -> Iterable[tuple[bool, T]]: + """Iterate and generate a tuple with a flag for last value.""" + iter_values = iter(values) + try: + previous_value = next(iter_values) + except StopIteration: + return + for value in iter_values: + yield False, previous_value + previous_value = value + yield True, previous_value + + + .. tab:: loop_first_last + + .. code-block:: python + + from __future__ import annotations + + from typing import Iterable, TypeVar + + T = TypeVar("T") + + + def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]: + """Iterate and generate a tuple with a flag for first and last value.""" + iter_values = iter(values) + try: + previous_value = next(iter_values) + except StopIteration: + return + first = True + for value in iter_values: + yield first, False, previous_value + first = False + previous_value = value + yield first, True, previous_value + + +Short-Circuit Search +++++++++++++++++++++ + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/short_circuit.py + :language: python3 + :start-after: # With Generator Short Circuit + :end-before: # Naive Approach + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/short_circuit.py + :language: python3 + :start-after: # Naive Approach + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/short_circuit.py + :language: python3 + + +Generic Solution +________________ + +.. code-block:: python + + from typing import Iterator, Any, Callable, TypeVar + + T = TypeVar('T') + + + def find_first( + iterable: Iterator[T], + condition: Callable[[T], bool], + terminal: Any = None + ) -> Any: + """ + Find the first item which meets a specific condition. + + Args: + iterable: + which will be searched. + condition: + which needs to be meet by the item. + terminal: + value which will be returned if the condition did not return True + for any item in the iterable. + + Returns: + The first item in the iterable for which the condition returns True. + If the condition does not return True for any item in the iterable, the + terminal value is returned. + + Examples: + + >>> numbers = [1, 2, 3, 4 ,5 ,6] + >>> value = find_first(numbers, condition=lambda _: True) + >>> value == 1 + True + + >>> numbers = [1, 2, 3, 4 ,5 ,6] + >>> value = find_first(numbers, condition=lambda item: item % 3 == 0) + >>> value == 3 + True + + >>> value = find_first(numbers, condition=lambda item: item % 7 == 0) + >>> value is None + True + + >>> value = find_first(numbers, condition=lambda item: item % 7 == 0, terminal=7) + >>> value == 7 + True + """ + items = (item for item in iterable if condition(item)) + try: + item = next(items) + except StopIteration: + item = terminal + return item + +Context Manager ++++++++++++++++ +Factor out context into a context manager. + +.. tab:: ✅ With Context Manager + + .. tab:: Python 3.8 - 3.10 + + .. literalinclude:: ../../../_static/idioms/context_manager.py + :language: python3 + :start-after: # With Context Manager + :end-before: # With Python 3.11 + + .. tab:: Python 3.11 + + .. literalinclude:: ../../../_static/idioms/context_manager.py + :language: python3 + :start-after: # With Python 3.11 + :end-before: # Naive Approach + +.. tab:: ❌ Naive Approach + + .. literalinclude:: ../../../_static/idioms/context_manager.py + :language: python3 + :start-after: # Naive Approach + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/context_manager.py + :language: python3 + + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + +Decorator ++++++++++ +Factor out unrelated repetitive work into a decorator. + +.. tab:: ✅ With Decorator + + .. literalinclude:: ../../../_static/idioms/context_decorator.py + :language: python3 + :start-after: # With Decorator + :end-before: # Naive Approach + +.. tab:: ❌ Naive Approach + + .. literalinclude:: ../../../_static/idioms/context_decorator.py + :language: python3 + :start-after: # Naive Approach + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/context_decorator.py + :language: python3 + + +.. _Raymond Hettinger: https://github.com/rhettinger +.. _Will McGugan: https://github.com/willmcgugan +.. _Transform Code into Beautiful, Idiomatic Python: https://www.youtube.com/watch?v=OSGv2VnC0go> +.. _Transform Python Slides: https://speakerdeck.com/pyconslides/transforming-code-into-beautiful-idiomatic-python-by-raymond-hettinger-1 +.. _Stealing Open Source code from Textual: https://textual.textualize.io/blog/2022/11/20/stealing-open-source-code-from-textual/ diff --git a/doc/styleguide/guides/idioms/strings.rst b/doc/styleguide/guides/idioms/strings.rst new file mode 100644 index 00000000..9db40d69 --- /dev/null +++ b/doc/styleguide/guides/idioms/strings.rst @@ -0,0 +1,90 @@ +Strings +------- + +Concatenation ++++++++++++++ +Use *.join* to to concatenate strings + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/concat.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/concat.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/concat.py + :language: python3 + + +**💡 learnt from:** + +* Source: `Raymond Hettinger`_ +* Reference: `Transform Python Slides`_ + +f-Strings ++++++++++ +Use f-String for simple placeholder expressions + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/fstring.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/fstring.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/fstring.py + :language: python3 + + +String.format ++++++++++++++ +Use the string format function for complex expressions + +.. tab:: ✅ Good + + .. literalinclude:: ../../../_static/idioms/format.py + :language: python3 + :start-after: # Good + :end-before: # Bad + +.. tab:: ❌ Bad + + .. literalinclude:: ../../../_static/idioms/format.py + :language: python3 + :start-after: # Bad + +.. tab:: 🎭 Compare + + .. literalinclude:: ../../../_static/idioms/format.py + :language: python3 + + +**💡 learnt from:** + +* Source: `Robert Smallshire`_ , `Austin Bingham`_ +* References: `Rober Smallshire - Courses`_, `Austin Bingham - Courses`_ + +.. _Raymond Hettinger: https://github.com/rhettinger +.. _Austin Bingham: https://leanpub.com/u/abingham +.. _Robert Smallshire: https://leanpub.com/u/robert-smallshire + +.. _Transform Code into Beautiful, Idiomatic Python: https://www.youtube.com/watch?v=OSGv2VnC0go> +.. _Transform Python Slides: https://speakerdeck.com/pyconslides/transforming-code-into-beautiful-idiomatic-python-by-raymond-hettinger-1 +.. _Austin Bingham - Courses: https://www.pluralsight.com/authors/austin-bingham +.. _Rober Smallshire - Courses: https://www.pluralsight.com/authors/robert-smallshire diff --git a/doc/styleguide/guides/style.rst b/doc/styleguide/guides/style.rst new file mode 100644 index 00000000..51241a5a --- /dev/null +++ b/doc/styleguide/guides/style.rst @@ -0,0 +1,64 @@ +Styleguide +========== + +TL;DR +______ +Read PEP8 and the `Google Styleguide`_ for a general orientation. Most rules should be check by our tooling. + +Style +_____ +.. note:: + + This guide is a work in progress, so if you see something which isn't addressed or should be changed + feel free to crate a Issue/PR to start a discussion on the topic. + + .. attention:: + + Please search the Issues & PR's beforehand, to make sure that your case + is bringing something new to the table. + +How to apply style & coding conventions +--------------------------------------- + +#. Apply formatters and linters +#. Check if we have defined an explicit rule on this in the styleguide +#. Consult the `Google Styleguide`_ +#. Consult `PEP 8`_ + + +Idioms +_______ + +.. toctree:: + :maxdepth: 2 + + idioms/idioms + + +References & Further Reading +____________________________ +* `Google Styleguide`_ +* `PEP 8 `_ +* `Python Idioms`_ +* `Python Like You Mean It`_ +* `Python Programming Idioms`_ + +Talks +_____ +* `Transform Code into Beautiful, Idiomatic Python`_ +* `Stop Writing Classes`_ +* `Refactoring Python`_ + +.. _Raymond Hettinger: https://github.com/rhettinger + +.. _Google Styleguide: https://google.github.io/styleguide/pyguide.html +.. _PEP 8: https://peps.python.org/pep-0008/ +.. _Python Idioms: https://gist.github.com/0x4D31/f0b633548d8e0cfb66ee3bea6a0deff9 +.. _Python Like You Mean It: http://www.pythonlikeyoumeanit.com/module_2.html> +.. _Python Programming Idioms: https://en.wikibooks.org/wiki/Python_Programming/Idioms + +.. _Transform Code into Beautiful, Idiomatic Python: https://www.youtube.com/watch?v=OSGv2VnC0go> +.. _Transform Python Slides: https://speakerdeck.com/pyconslides/transforming-code-into-beautiful-idiomatic-python-by-raymond-hettinger-1 +.. _Stop Writing Classes: https://www.youtube.com/watch?v=o9pEzgHorH0 +.. _Refactoring Python: https://www.youtube.com/watch?v=D_6ybDcU5gc + diff --git a/doc/styleguide/index.rst b/doc/styleguide/index.rst new file mode 100644 index 00000000..ce95d79b --- /dev/null +++ b/doc/styleguide/index.rst @@ -0,0 +1,67 @@ +.. _styleguide: + + +:octicon:`paintbrush` Python Styleguide +======================================= + +.. toctree:: + :maxdepth: 2 + :hidden: + + guides/style + + +Welcome +------- + +Welcome to the Exasol_ python-styleguide. +This styleguide is not intend to be the holy bible on how to do each and every single detail in python, +it rather tries to reley on existing documents, guides and references (e.g. pep8_). +This guide rather is adds useful additions or exceptions where Exasol_ is diverging from the *"norm"*. + +.. figure:: https://imgs.xkcd.com/comics/standards.png + :alt: xkcd-1445 + :target: https://xkcd.com/1445/ + + source: `xkcd.com `_ + license: `CC BY-NC 2.5 `_ + + +.. note:: + + This guide is a rolling release, so it always reflects the latest status quo. Still it may also happen that + the information is missing or incomplete, in that case we want to encourage the reader to either create a PR + or an Issue, to either track or fix those cases. + +Before you start +---------------- +As already mentioned this guide is a extension rather than a reference manual so before you jump right in, +we want you to take a moment and read through **The Zen of Python** if you haven't read it before. + +.. code-block:: + + >>> import this + The Zen of Python, by Tim Peters + + Beautiful is better than ugly. + Explicit is better than implicit. + Simple is better than complex. + Complex is better than complicated. + Flat is better than nested. + Sparse is better than dense. + Readability counts. + Special cases aren't special enough to break the rules. + Although practicality beats purity. + Errors should never pass silently. + Unless explicitly silenced. + In the face of ambiguity, refuse the temptation to guess. + There should be one-- and preferably only one --obvious way to do it. + Although that way may not be obvious at first unless you're Dutch. + Now is better than never. + Although never is often better than *right* now. + If the implementation is hard to explain, it's a bad idea. + If the implementation is easy to explain, it may be a good idea. + Namespaces are one honking great idea -- let's do more of those! + +.. _Exasol: https://www.exasol.com/ +.. _pep8: https://peps.python.org/pep-0008/ diff --git a/noxconfig.py b/noxconfig.py index a78b5779..f3f3dc49 100644 --- a/noxconfig.py +++ b/noxconfig.py @@ -35,7 +35,7 @@ class Config: root: Path = Path(__file__).parent doc: Path = Path(__file__).parent / "doc" version_file: Path = Path(__file__).parent / "exasol" / "toolbox" / "version.py" - path_filters: Iterable[str] = ("dist", ".eggs", "venv", "metrics-schema", "project-template") + path_filters: Iterable[str] = ("dist", ".eggs", "venv", "metrics-schema", "project-template", "idioms") plugins = [UpdateTemplates] diff --git a/poetry.lock b/poetry.lock index 3434f829..00522467 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1491,6 +1491,24 @@ theme-pydata = ["pydata-sphinx-theme (>=0.13.0,<0.14.0)"] theme-rtd = ["sphinx-rtd-theme (>=1.0,<2.0)"] theme-sbt = ["sphinx-book-theme (>=1.0,<2.0)"] +[[package]] +name = "sphinx-inline-tabs" +version = "2023.4.21" +description = "Add inline tabbed content to your Sphinx documentation." +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinx_inline_tabs-2023.4.21-py3-none-any.whl", hash = "sha256:06809ac613f7c48ddd6e2fa588413e3fe92cff2397b56e2ccf0b0218f9ef6a78"}, + {file = "sphinx_inline_tabs-2023.4.21.tar.gz", hash = "sha256:5df2f13f602c158f3f5f6c509e008aeada199a8c76d97ba3aa2822206683bebc"}, +] + +[package.dependencies] +sphinx = ">=3" + +[package.extras] +doc = ["furo", "myst-parser"] +test = ["pytest", "pytest-cov", "pytest-xdist"] + [[package]] name = "sphinxcontrib-applehelp" version = "1.0.4" @@ -1722,4 +1740,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "0c3d812d374a65bb11dd86cd5ee2029011e536ed80836fada04c7d8ad55e774f" +content-hash = "ca885528be7bd58edf02ee1880c86d73109549c8d04f5ab89defb1d5112f13eb" diff --git a/pyproject.toml b/pyproject.toml index c202b1e0..0a2911d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,26 +34,27 @@ keywords = [ "Changelog" = "https://exasol.github.io/python-toolbox/changelog.html" [tool.poetry.dependencies] -python = "^3.8" -nox = ">=2022.8.7" -sphinx = ">=5.3,<8" -pytest = ">=7.2.2,<9" -mypy = ">=0.971" -sphinx-copybutton = "^0.5.0" -furo = ">=2022.9.15" -pyupgrade = ">=2.38.2,<4.0.0" black = ">=24.1.0" -isort = "^5.12.0" -pre-commit = ">=3.1.1,<4" coverage = ">=6.4.4,<8.0.0" -pylint = ">=2.15.4" -typer = {extras = ["all"], version = ">=0.7.0"} -prysk = {extras = ["pytest-plugin"], version = ">0.17.0,<1"} +furo = ">=2022.9.15" importlib-resources = ">=5.12.0" +isort = "^5.12.0" +mypy = ">=0.971" myst-parser = ">=2.0.0,<4" +nox = ">=2022.8.7" pluggy = "^1.5.0" +pre-commit = ">=3.1.1,<4" +prysk = {extras = ["pytest-plugin"], version = ">0.17.0,<1"} +pylint = ">=2.15.4" +pytest = ">=7.2.2,<9" +python = "^3.8" +pyupgrade = ">=2.38.2,<4.0.0" shibuya = ">=2024.5.14" +sphinx = ">=5.3,<8" +sphinx-copybutton = "^0.5.0" +sphinx-inline-tabs = "^2023.4.21" sphinx-design = ">=0.5.0,<1" +typer = {extras = ["all"], version = ">=0.7.0"} [tool.poetry.group.dev.dependencies]