267 |
272 |
273 |
274 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | import os
10 | import sys
11 | # Use this line if you chose to separate source and build directories
12 | sys.path.insert(0, os.path.abspath('../..'))
13 | # Use this line if you chose to NOT separate source and build directories
14 | # sys.path.insert(0, os.path.abspath('..'))
15 |
16 | project = 'Python Best Practices'
17 | copyright = '2023, Claudio Shigueo Watanabe'
18 | author = 'Claudio Shigueo Watanabe'
19 | release = '1.0.0'
20 |
21 | # -- General configuration ---------------------------------------------------
22 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
23 |
24 | extensions = ['sphinx.ext.autodoc']
25 |
26 | templates_path = ['_templates']
27 | exclude_patterns = []
28 |
29 |
30 |
31 | # -- Options for HTML output -------------------------------------------------
32 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
33 |
34 | html_theme = 'sphinx_rtd_theme'
35 | html_static_path = ['_static']
36 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. Python Best Practices documentation master file, created by
2 | sphinx-quickstart on Mon Apr 10 12:54:46 2023.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to Python Best Practices's documentation!
7 | =================================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 |
14 |
15 | Indices and tables
16 | ==================
17 |
18 | * :ref:`genindex`
19 | * :ref:`modindex`
20 | * :ref:`search`
21 |
--------------------------------------------------------------------------------
/docs/source/modules.rst:
--------------------------------------------------------------------------------
1 | python-best-practices
2 | =====================
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | testing
8 |
--------------------------------------------------------------------------------
/docs/source/testing.rst:
--------------------------------------------------------------------------------
1 | testing package
2 | ===============
3 |
4 | Submodules
5 | ----------
6 |
7 | testing.conftest module
8 | -----------------------
9 |
10 | .. automodule:: testing.conftest
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | testing.mocking module
16 | ----------------------
17 |
18 | .. automodule:: testing.mocking
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | testing.mocking\_test module
24 | ----------------------------
25 |
26 | .. automodule:: testing.mocking_test
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | testing.pytesting module
32 | ------------------------
33 |
34 | .. automodule:: testing.pytesting
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 | testing.pytesting\_test module
40 | ------------------------------
41 |
42 | .. automodule:: testing.pytesting_test
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
47 | Module contents
48 | ---------------
49 |
50 | .. automodule:: testing
51 | :members:
52 | :undoc-members:
53 | :show-inheritance:
54 |
--------------------------------------------------------------------------------
/documenting/documenting.py:
--------------------------------------------------------------------------------
1 | """ This class is an example on how to document Python code
2 | """
3 |
4 |
5 | def transform_int_to_str(number: int) -> str:
6 | """ Transform a int into a string
7 |
8 | :param number: number to transform
9 | :returns: A string with 'The input was ' concatenated with the parameter
10 | """
11 | return f"The input was {number}"
12 |
13 |
14 | class Transformer():
15 | """
16 | Small description of the class
17 |
18 | Larger description of the class. This class is just an example of how to
19 | document Python code.
20 | """
21 | def transform_float_less_than_100_to_int(self, number: float) -> int:
22 | """ Transform a number into a string
23 |
24 | :param number: number to transform
25 | :raises ValueError: if the parameter is greater than or equal to 100
26 | :returns: A string with 'The input was ' concatenated with the
27 | parameter
28 | """
29 | if number < 100:
30 | return int(number)
31 | else:
32 | raise ValueError(f"{number} is greater than or equal to 100")
33 |
--------------------------------------------------------------------------------
/flask_examples/flask_basic.py:
--------------------------------------------------------------------------------
1 | """ Basic Flask Example
2 | """
3 |
4 |
5 | from flask import Flask
6 |
7 |
8 | def create_basic_flask_app():
9 | """ Basic Flask Example
10 | """
11 | app_basic = Flask(__name__)
12 |
13 | @app_basic.route("/")
14 | def index():
15 | """ Index route
16 | """
17 | return "Hello World!"
18 |
19 | return app_basic
20 |
21 |
22 | if __name__ == "__main__":
23 | app = create_basic_flask_app()
24 | app.run(debug=True)
25 |
--------------------------------------------------------------------------------
/flask_examples/flask_basic_test.py:
--------------------------------------------------------------------------------
1 | """ Basic Flask Example Test
2 | """
3 |
4 |
5 | import pytest
6 | from flask_basic import create_basic_flask_app
7 |
8 |
9 | @pytest.fixture()
10 | def app_flask():
11 | """ Instance Basic Flask App to test
12 | """
13 | app = create_basic_flask_app()
14 | app.config.update({
15 | "TESTING": True,
16 | })
17 |
18 | yield app
19 |
20 |
21 | @pytest.fixture()
22 | def client(app_flask):
23 | """ Client test for Basic Flask App
24 | """
25 | return app_flask.test_client()
26 |
27 |
28 | def test_request_example(client):
29 | """ Test request example
30 | """
31 | response = client.get("/")
32 | assert b"Hello World!" in response.data
33 |
--------------------------------------------------------------------------------
/pep8/blank_lines.py:
--------------------------------------------------------------------------------
1 | """ This is a Python module example to show PEP 8 blank lines code layout.
2 | """
3 |
4 | # importing os module
5 | import os
6 |
7 |
8 | class CurrentDirectory:
9 | """ This is a class example that shows the current directory
10 | See that there are two blank lines before a class
11 | """
12 | def __init__(self) -> None:
13 | self.current_directory = os.getcwd()
14 |
15 | def show_directory(self):
16 | """ This is a method example that shows the current directory
17 | See that there is only one blank line before a method
18 | """
19 | return self.current_directory
20 |
21 | def list_directory(self):
22 | """ This is a method example that lists the current directory
23 | """
24 | directory_listing = os.listdir(self.current_directory)
25 |
26 | # This blank line above is optional.
27 | # We can use these optional blank lines to organize the code.
28 | return directory_listing
29 |
30 |
31 | def top_level_function():
32 | """ This is a function example of a top level function.
33 | See that there are two blank lines before a top level function
34 | """
35 | return None
36 |
--------------------------------------------------------------------------------
/pep8/blank_lines_test.py:
--------------------------------------------------------------------------------
1 | """ Tests for blank_lines module
2 | """
3 |
4 | # importing os module
5 | import os
6 | from blank_lines import CurrentDirectory
7 |
8 |
9 | def test_current_directory():
10 | """ Test for CurrentDirectory class
11 | """
12 | current_directory = CurrentDirectory()
13 |
14 | assert current_directory.show_directory() == os.getcwd()
15 |
--------------------------------------------------------------------------------
/pep8/line_breaking.py:
--------------------------------------------------------------------------------
1 | """ This module shows examples of how to break likes when our code passes the
2 | 79 character limit.
3 | """
4 |
5 |
6 | def first_function_with_very_long_name(
7 | first_argument: str,
8 | second_argument: int) -> str:
9 | """ This function would pass the 79 character limit.
10 | """
11 | return f"{first_argument} {second_argument}"
12 |
13 |
14 | def second_function_with_very_long_name(
15 | first_argument: str,
16 | second_argument: int
17 | ) -> str:
18 | """ This function would pass the 79 character limit.
19 | """
20 | return f"{first_argument} {second_argument}"
21 |
22 |
23 | def third_function_with_very_long_name(
24 | first_argument: str, second_argument: int) -> str:
25 | """ This function would pass the 79 character limit.
26 | """
27 | return f"{first_argument} {second_argument}"
28 |
29 |
30 | def fourth_function_with_very_long_name(
31 | first_argument: str,
32 | second_argument: int) -> str:
33 | """ This function would pass the 79 character limit.
34 | """
35 | return f"{first_argument} {second_argument}"
36 |
37 |
38 | def fifth_function_with_very_long_name(first_argument: str,
39 | second_argument: int) -> str:
40 | """ This function would pass the 79 character limit.
41 | """
42 | return f"{first_argument} {second_argument}"
43 |
44 |
45 | def function_with_math_operators(
46 | first_long_argument, second_long_argument,
47 | third_long_argument, fourth_long_argument):
48 | """ This function content would pass the 79 character limit.
49 | """
50 | # It is recommended that we break we place the math operators at the
51 | # beginning of the line, not at the end of the line.
52 | return (first_long_argument
53 | + second_long_argument
54 | - third_long_argument
55 | * fourth_long_argument)
56 |
57 |
58 | def function_example_with_backslash(number_to_check):
59 | """ This function content would pass the 79 character limit.
60 | """
61 | return number_to_check < 10 or 21 <= number_to_check < 20 or \
62 | number_to_check > 100
63 |
64 |
65 | def function_with_if_examples(number_to_check):
66 | """ This function content would pass the 79 character limit.
67 | """
68 | # Notice that the conditions aren't at the same level of parenthesis
69 | if (
70 | number_to_check < 10 or
71 | 21 <= number_to_check < 20 or
72 | number_to_check > 100):
73 | return True
74 | # Notice that the closing parenthesis is at the same level of the oppening
75 | # parenthesis
76 | if (
77 | number_to_check < 10 or
78 | 21 <= number_to_check < 20 or
79 | number_to_check > 100
80 | ):
81 | return True
82 | # Notice that the closing parenthesis is at the same level of the if clause
83 | if (
84 | number_to_check < 10 or
85 | 21 <= number_to_check < 20 or
86 | number_to_check > 100
87 | ):
88 | return True
89 | # This is my least preferred one
90 | if (number_to_check < 10 or
91 | 21 <= number_to_check < 20 or
92 | number_to_check > 100):
93 | return True
94 |
95 |
96 | # This is an example of a long string
97 | LONG_STRING = ("This is an example of a very long Python string. We can use "
98 | "parenthesis, and single or double quotes for that."
99 | )
100 |
101 |
102 | # This is an example of a long string using f-strings
103 | a_value = input()
104 | long_fstring = ("This is an example of a very long Python string. We can use "
105 | f"parenthesis, and single or double quotes for that. {a_value}"
106 | )
107 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | alabaster==0.7.13
2 | astroid==2.14.2
3 | attrs==22.2.0
4 | Babel==2.12.1
5 | blinker==1.6.2
6 | Cerberus==1.3.4
7 | certifi==2022.12.7
8 | cfgv==3.3.1
9 | charset-normalizer==3.1.0
10 | click==8.1.3
11 | colorama==0.4.6
12 | dill==0.3.6
13 | distlib==0.3.6
14 | docutils==0.18.1
15 | exceptiongroup==1.1.0
16 | filelock==3.9.0
17 | Flask==2.3.2
18 | identify==2.5.18
19 | idna==3.4
20 | imagesize==1.4.1
21 | importlib-metadata==6.3.0
22 | iniconfig==2.0.0
23 | isort==5.12.0
24 | itsdangerous==2.1.2
25 | Jinja2==3.1.2
26 | lazy-object-proxy==1.9.0
27 | MarkupSafe==2.1.2
28 | mccabe==0.7.0
29 | mypy==1.1.1
30 | mypy-extensions==1.0.0
31 | nodeenv==1.7.0
32 | packaging==23.0
33 | pep8==1.7.1
34 | platformdirs==3.0.0
35 | pluggy==1.0.0
36 | pre-commit==3.1.1
37 | pycodestyle==2.10.0
38 | Pygments==2.15.0
39 | pylint==2.16.2
40 | pylint-pytest==1.1.2
41 | pytest==7.2.1
42 | pytest-mock==3.10.0
43 | PyYAML==6.0
44 | requests==2.28.2
45 | snowballstemmer==2.2.0
46 | Sphinx==6.1.3
47 | sphinx-rtd-theme==1.2.0
48 | sphinxcontrib-applehelp==1.0.4
49 | sphinxcontrib-devhelp==1.0.2
50 | sphinxcontrib-htmlhelp==2.0.1
51 | sphinxcontrib-jquery==4.1
52 | sphinxcontrib-jsmath==1.0.1
53 | sphinxcontrib-qthelp==1.0.3
54 | sphinxcontrib-serializinghtml==1.1.5
55 | tomli==2.0.1
56 | tomlkit==0.11.6
57 | types-colorama==0.4.15.11
58 | types-docutils==0.20.0.1
59 | types-Pygments==2.15.0.1
60 | types-setuptools==67.7.0.2
61 | typing_extensions==4.5.0
62 | urllib3==1.26.15
63 | virtualenv==20.19.0
64 | Werkzeug==2.3.4
65 | wrapt==1.15.0
66 | zipp==3.15.0
67 |
--------------------------------------------------------------------------------
/solid/dependency_inversion_principle_dip.py:
--------------------------------------------------------------------------------
1 | """ This module has an example of the Dependency Inversion Principle(DIP) in
2 | Python.
3 | """
4 |
5 |
6 | from abc import ABC, abstractmethod
7 |
8 |
9 | # This is a tightly coupled example that violates the Dependency Inversion
10 | # Principle (DIP) as the class MySQLStorageBadExample is called directly
11 | # without using an abstraction.
12 | class MySQLStorageBadExample():
13 | """ This is the implementation of MySQL storage.
14 | """
15 |
16 | def get(self):
17 | """ Get data
18 | """
19 | print("Get data from MySQL database")
20 |
21 | def set(self, data):
22 | """ Set data
23 | """
24 | print(f"Set {data} into MySQL database")
25 |
26 |
27 | class UseCaseBadExample():
28 | """ This is the main class of the application.
29 | """
30 | def run(self):
31 | """ This is the main method of the use case.
32 | """
33 | bad_storage = MySQLStorageBadExample()
34 | bad_storage.get()
35 | bad_storage.set({"data": "Hello World"})
36 |
37 |
38 | if __name__ == "__main__":
39 | use_case_bad_example = UseCaseBadExample()
40 | use_case_bad_example.run()
41 |
42 |
43 | # This is a loosely coupled example that follows the Dependency Inversion
44 | # Principle (DIP) as the class MySQLStorage is called using the
45 | # abstract class Storage.
46 | class Storage (ABC):
47 | """ This is the abstract class for the storage.
48 | """
49 |
50 | @abstractmethod
51 | def get(self):
52 | """ Get data
53 | """
54 |
55 | @abstractmethod
56 | def set(self, data):
57 | """ Set data
58 | """
59 |
60 |
61 | class MySQLStorage(Storage):
62 | """ This is the implementation of MySQL storage.
63 | """
64 |
65 | def get(self):
66 | print("Get data from MySQL database")
67 |
68 | def set(self, data):
69 | print(f"Set {data} into MySQL database")
70 |
71 |
72 | class CSVFileStorage(Storage):
73 | """ This is the implementation of CSV storage.
74 | """
75 |
76 | def get(self):
77 | print("Get data from CSV storage")
78 |
79 | def set(self, data):
80 | print(f"Set {data} into CSV storage")
81 |
82 |
83 | class UseCase():
84 | """ This is the main class of the application.
85 | """
86 | def run(self, storage: Storage):
87 | """ This is the main method of the use case.
88 | """
89 | storage.get()
90 | storage.set({"data": "Hello World"})
91 |
92 |
93 | if __name__ == "__main__":
94 | use_case = UseCase()
95 | use_case.run(MySQLStorage())
96 | use_case.run(CSVFileStorage())
97 |
98 | # Consequently, following the Dependency Inversion Principle we can improve our
99 | # code maintainability and flexibility because it will depend less on lower
100 | # classes (like MySQLStorage in our example) and we can even use more than one
101 | # lower class without changing the higher class (eg UseCase).
102 |
--------------------------------------------------------------------------------
/solid/interface_segregation_principle_isp.py:
--------------------------------------------------------------------------------
1 | """ This module has examples of the Interface Segregation Principle (ISP)
2 | in Python.
3 | """
4 |
5 |
6 | from abc import ABC, abstractmethod
7 |
8 |
9 | class EmployeeBadExample(ABC):
10 | """ An abstract class example that doesn't follow the Interface Segregation
11 | Principle as it has methods that aren't used by its child classes
12 | """
13 |
14 | @abstractmethod
15 | def write_code(self):
16 | """ Write code method
17 | """
18 |
19 | @abstractmethod
20 | def cook(self):
21 | """ Cook method
22 | """
23 |
24 | @abstractmethod
25 | def lead(self):
26 | """ Lead method
27 | """
28 |
29 |
30 | class ChefBadExample(EmployeeBadExample):
31 | """ Chef Class
32 | """
33 | def write_code(self):
34 | raise NotImplementedError("Chefs don't write code.")
35 |
36 | def cook(self):
37 | print("I am cooking")
38 |
39 | def lead(self):
40 | print("I am leading")
41 |
42 |
43 | # Those classes from now on follow the Interface Segregation Principle
44 | # Instead of having one abstract class for all employees, we divided them by
45 | # roles and the child class uses multiple inheritance.
46 | # If you don’t know what multiple inheritance is, the Chef class has multiple
47 | # inheritance as it has two parent classes. Chef class inherits the lead method
48 | # from the Leader class and the cook method from the Cook class.
49 | class Cook(ABC):
50 | """ An abstract class representing a cook (person who cooks)
51 | """
52 | @abstractmethod
53 | def cook(self):
54 | """ Cook method
55 |
56 | """
57 |
58 |
59 | class Leader(ABC):
60 | """ An abstract class representing a leader
61 | """
62 |
63 | @abstractmethod
64 | def lead(self):
65 | """ Lead method
66 | """
67 |
68 |
69 | class Chef(Cook, Leader):
70 | """ Chef Class
71 | """
72 | def lead(self):
73 | print("I am leading")
74 |
75 | def cook(self):
76 | print("I am cooking")
77 |
78 |
79 | # Alternatively, the method could be implemented directly on the child class
80 | # without relating it to the parent class. The fly method of the Bird class
81 | # below is an example of this approach.
82 | class Animal(ABC):
83 | """ An abstract class representing a leader
84 | """
85 |
86 | @abstractmethod
87 | def eat(self):
88 | """ Eat method
89 | """
90 |
91 |
92 | class Bird(Animal):
93 | """ Bird Class
94 | """
95 | def eat(self):
96 | print("I am eating")
97 |
98 | def fly(self):
99 | """ Fly method
100 | """
101 | print("I am flying")
102 |
--------------------------------------------------------------------------------
/solid/liskov_substitution_principle_lsp.py:
--------------------------------------------------------------------------------
1 | """ This module has an example of the Liskov Substitution Principle(LSP) in
2 | Python.
3 | """
4 |
5 |
6 | # The example below violates The Liskov Substitution Principle as the calculate
7 | # Father class returns a value and the method from SonBadExample class prints
8 | # it.
9 | class Father():
10 | """ Example of a base class
11 | """
12 | def add(self, first: float, second: float) -> float:
13 | """ Do a calculation
14 | """
15 | return first + second
16 |
17 |
18 | class SonBadExample(Father):
19 | """ Example of a child class that violates the Liskov Substitution
20 | Principle
21 | """
22 | def add(self, first: float, second: float) -> None: # type: ignore
23 | """ Do a calculation
24 | """
25 | print(first + second)
26 |
27 |
28 | # On the other hand, the class below doesn't do polymorphism but creates a new
29 | # method which follows the Liskov Substitution Principle.
30 | # If you don’t know what polymorphism is, we did polymorphism in the previous
31 | # example when we overrided the method. This is, we redefine in the child class
32 | # a method (add) already defined in the parent class.
33 | class Son(Father):
34 | """ Example of a child class that follows the Liskov Substitution Principle
35 | """
36 | def print_addition(self, first: float, second: float) -> None:
37 | """ Do a calculation
38 | """
39 | print(self.add(first, second))
40 |
--------------------------------------------------------------------------------
/solid/open_closed_principle_ocp/classes_that_violate_srp.py:
--------------------------------------------------------------------------------
1 | """ This module has an example of the Open/Closed Principle in Python where
2 | classes DON'T follow the Single Responsibility Principle
3 | """
4 |
5 |
6 | # This UserBad class is not following the Open/Closed Principle because if
7 | # we need to add a responsibility, we would need to modify the class.
8 | # For example, if we add the feature of sending email like the example shown on
9 | # the SRP example, we need to modify the UserBad class.
10 | class UserBad():
11 | """ This is an example of a class in Python that is NOT following the
12 | Single Responsibility Principle and Open/Closed Principle
13 | """
14 | def __init__(self, username, name):
15 | self.username = username
16 | self.name = name
17 |
18 | def save(self):
19 | """ This method would send a confirmation email
20 | """
21 | print("Saving data into database")
22 |
23 |
24 | # Here we separated the responsibilities, each responsibility in your
25 | # respective class. So, this example follows the Open/Closed Principle and
26 | # Single Responsibility Principle. Before, we had two reasons to change the
27 | # UserBad class, changing user properties and saving to database. Now, each of
28 | # these has its own class and if we need an additional responsibility, we would
29 | # create a new class.
30 | class User():
31 | """ This class is an example of the Open/Closed Principle in Python
32 | """
33 | def __init__(self, username, name):
34 | self.username = username
35 | self.name = name
36 |
37 |
38 | class UserDatabase():
39 | """ This class is an example of the Open/Closed Principle in Python
40 | """
41 | def save(self, user: User):
42 | """ This method would save data to the database
43 | """
44 | print(f"Saving {user} to database")
45 |
--------------------------------------------------------------------------------
/solid/open_closed_principle_ocp/classes_tightly_coupled.py:
--------------------------------------------------------------------------------
1 | """ This module has an example of the Open/Close Principle in Python where
2 | classes are tightly coupled.
3 | """
4 |
5 |
6 | class UserBadExample():
7 | """ This is an example of a class in Python that is NOT following the
8 | Open/Closed Principle
9 | """
10 | def __init__(self, username, name):
11 | self.username = username
12 | self.name = name
13 |
14 |
15 | # This function is tightly coupled to the UserBadExample class as we would need
16 | # to change the get_user_bad_example function if we need to change the
17 | # UserBadExample class. That also violates the Open/Close Principle.
18 | def get_user_bad_example(user_bad_example):
19 | """ This is an example of a Python function that is NOT following the
20 | Open/Closed Principle
21 | """
22 | return {
23 | "username": user_bad_example.username,
24 | "name": user_bad_example.name
25 | }
26 |
27 |
28 | # Transforming the function into a method of the class solves the previous
29 | # problem and follows the Open/Close Principle.
30 | class User():
31 | """ This is an example of a Python class that follows the
32 | Open/Closed Principle
33 | """
34 | def __init__(self, username, name):
35 | self.username = username
36 | self.name = name
37 |
38 | def get(self):
39 | """ Get user data """
40 | return {"username": self.username, "name": self.name}
41 |
--------------------------------------------------------------------------------
/solid/open_closed_principle_ocp/classes_with_conditional_logic.py:
--------------------------------------------------------------------------------
1 | """ This module has example of Open/Closed Principle in Python where Classes
2 | have conditional logic
3 | """
4 | from abc import ABC, abstractmethod
5 |
6 |
7 | class InterestRateBadExample():
8 | """ This is an example of a class in Python that is NOT following the
9 | Open/Closed Principle
10 | """
11 | def get_interest_rate(self, category):
12 | """ Method to get interest rate
13 | """
14 | if category == 'standard':
15 | return 0.03
16 | elif category == 'premium':
17 | return 0.05
18 |
19 |
20 | class InterestRate(ABC):
21 | """ This is an example of an abstract class in Python that is following the
22 | Open/Closed Principle
23 | """
24 | @abstractmethod
25 | def get_interest_rate(self):
26 | """ Method to get interest rate
27 | """
28 |
29 |
30 | class StandardInterestRate(InterestRate):
31 | """ This is an example of a class in Python that is following the
32 | Open/Closed Principle
33 | """
34 | def get_interest_rate(self):
35 | """ Method to get interest rate
36 | """
37 | return 0.03
38 |
39 |
40 | class PremiumInterestRate(InterestRate):
41 | """ This is an example of a class in Python that is following the
42 | Open/Closed Principle
43 | """
44 | def get_interest_rate(self):
45 | """ Method to get interest rate
46 | """
47 | return 0.05
48 |
--------------------------------------------------------------------------------
/solid/single_responsibility_principle_srp.py:
--------------------------------------------------------------------------------
1 | """ This module has examples of the Single Responsibility Principle in Python
2 | """
3 |
4 |
5 | # This UserBad class is not following the Single Responsibility Principle as it
6 | # has two responsibilities: managing user properties and sending confirmation
7 | # email.
8 | class UserBad():
9 | """ This is an example of a class in Python that is NOT following the
10 | Single Responsibility Principle
11 | """
12 | def __init__(self, username, email_address):
13 | self.username = username
14 | self.email_address = email_address
15 | self.send_confirmation_email()
16 |
17 | def send_confirmation_email(self):
18 | """ This method would send confirmation email
19 | """
20 | print(f"Sending confirmation email to {self.email_address}")
21 |
22 |
23 | # Here we separated the responsibilities, each responsibility in your
24 | # respective class. So, this example follows the Single Responsibility
25 | # Principle. Before, we had two reasons to change the UserBad class, changing
26 | # user properties and changing how the email is sent. Now, each of these has
27 | # its own class.
28 | class User():
29 | """ This class is an example of the Single Responsibility Principle in Python
30 | """
31 | def __init__(self, username, email_address):
32 | self.username = username
33 | self.email_address = email_address
34 | self.email = Email(email_address)
35 | self.email.send_confirmation_email()
36 |
37 |
38 | class Email():
39 | """ This class is an example of the Single Responsibility Principle in Python
40 | """
41 | def __init__(self, email_address):
42 | self.email_address = email_address
43 |
44 | def send_confirmation_email(self):
45 | """ This method would send confirmation email
46 | """
47 | print(f"Sending confirmation email to {self.email_address}")
48 |
--------------------------------------------------------------------------------
/testing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/claudiosw/python-best-practices/898eb103bfe0bb3259637628dea8544cae9019f9/testing/__init__.py
--------------------------------------------------------------------------------
/testing/conftest.py:
--------------------------------------------------------------------------------
1 | """ This file has fixtures for the tests included in this directory
2 | """
3 |
4 |
5 | import uuid
6 | from datetime import datetime
7 | import pytest
8 | from testing.pytesting import Product
9 |
10 |
11 | @pytest.fixture(autouse=True)
12 | def time_calculator():
13 | """ Example of an autouse fixture """
14 | # To see this, execute pytest with "-s" option
15 | print(f" Test started: {datetime.now().strftime('%d-%m-%Y %H:%M')} ")
16 | yield
17 | print(f" Test ended: {datetime.now().strftime('%d-%m-%Y %H:%M')} ")
18 |
19 |
20 | @pytest.fixture()
21 | def fixture_product_1():
22 | """ Example of a fixture of one product """
23 | return Product(
24 | id=uuid.uuid4(),
25 | price=1.11,
26 | name="test 1"
27 | )
28 |
29 |
30 | @pytest.fixture()
31 | def fixture_product_2():
32 | """ Example of a fixture of another product """
33 | return Product(
34 | id=uuid.uuid4(),
35 | price=11,
36 | name="test 2"
37 | )
38 |
39 |
40 | @pytest.fixture()
41 | def product_fixture_list(fixture_product_1, fixture_product_2):
42 | """ Example of a fixture composed of two other fixtures """
43 | return [fixture_product_1, fixture_product_2]
44 |
--------------------------------------------------------------------------------
/testing/mocking.py:
--------------------------------------------------------------------------------
1 | """ This module has supporting code to demonstrate testing
2 | """
3 |
4 |
5 | import random
6 | from abc import ABC, abstractmethod
7 |
8 |
9 | def run_dice() -> str:
10 | """ Simulate a dice roll
11 | :returns: 'Lucky' or 'Unlucky'
12 | """
13 | dice = random.randint(1, 6)
14 | if dice < 4:
15 | return "Unlucky"
16 | return "Lucky"
17 |
18 |
19 | class ExampleRepositoryInterface(ABC):
20 | """ Abstract class for ExampleRepository
21 | """
22 | @abstractmethod
23 | def create(self, name: str) -> str:
24 | """ Create Example
25 | :param name: Name
26 | :return: A String
27 | """
28 |
29 |
30 | class CreateExampleUseCase():
31 | """ Class CreateExampleUseCase
32 | """
33 | def __init__(self, repository: ExampleRepositoryInterface) -> None:
34 | self.repository = repository
35 |
36 | def execute(self, name: str) -> str:
37 | """ Method to execute use case
38 | :param name: Name of example
39 | :return: Str
40 | """
41 | return self.repository.create(name)
42 |
--------------------------------------------------------------------------------
/testing/mocking_pytest-mock_test.py:
--------------------------------------------------------------------------------
1 | """ In this file we have tests that use pytest-mock
2 | """
3 | from testing import mocking
4 | from testing.mocking import ExampleRepositoryInterface
5 |
6 |
7 | def test_mock_patch_function_with_dice(mocker):
8 | """ Testing run_dice function
9 | """
10 | # We say randint should return 4 then we execute the run_dice function.
11 | mocker.patch(
12 | # api_call is from slow.py but imported to main.py
13 | 'testing.mocking.random.randint',
14 | return_value=4
15 | )
16 | # Testing if run_dice returns "Lucky".
17 | assert mocking.run_dice() == "Lucky"
18 | assert mocking.random.randint.call_count == 1 # pylint: disable=E1101
19 | assert mocking.random.randint.call_once_with(1, 6) # pylint: disable=E1101
20 | assert mocking.random.randint.call_with(1, 6) # pylint: disable=E1101
21 |
22 | # We say randint should return 3 then we execute the run_dice function.
23 | mocker.patch(
24 | # api_call is from slow.py but imported to main.py
25 | 'testing.mocking.random.randint',
26 | return_value=3
27 | )
28 | # Testing if run_dice returns "Unlucky".
29 | assert mocking.run_dice() == "Unlucky"
30 | assert mocking.random.randint.call_count == 1 # pylint: disable=E1101
31 | assert mocking.random.randint.call_with(1, 6) # pylint: disable=E1101
32 |
33 | assert mocking.run_dice() == "Unlucky"
34 | assert mocking.random.randint.call_count == 2 # pylint: disable=E1101
35 | # We disable the E1101 pylint message because pylint can't figure out what
36 | # mock is doing behind the scene in a sane way.This problem only occurs
37 | # when you're mocking a module. Pylint correctly finds the type of the mock
38 | # if you're mocking a function, class or method.
39 |
40 |
41 | def test_create_example(mocker):
42 | """ Example of a test passing a mock as a parameter
43 | """
44 | # Mocking the object
45 | repository_mock = mocker.patch.object(
46 | ExampleRepositoryInterface,
47 | "create"
48 | )
49 | # Defining the result it will return
50 | repository_mock.create.return_value = "Result Example created"
51 | # Executing the test
52 | use_case = mocking.CreateExampleUseCase(repository_mock)
53 | result = use_case.execute("Name Example")
54 | # Check if the mocked method of the object received the correct parameter
55 | repository_mock.create.assert_called_once_with("Name Example")
56 | # Check if the use case passed the result of the mocked object correctly
57 | assert result == "Result Example created"
58 |
--------------------------------------------------------------------------------
/testing/mocking_test.py:
--------------------------------------------------------------------------------
1 | """ Tests to demonstrate testing.
2 | """
3 |
4 |
5 | import unittest
6 | from unittest.mock import Mock, patch
7 | from testing import mocking
8 |
9 |
10 | def test_mock_function():
11 | """ Test that a mock function is called
12 | """
13 | object_to_mock = Mock()
14 | object_to_mock.method_example("parameter example")
15 |
16 | assert object_to_mock.method_example.call_count == 1
17 |
18 | object_to_mock.method_example.assert_called()
19 | object_to_mock.method_example.assert_called_once()
20 | object_to_mock.method_example.assert_called_with("parameter example")
21 | object_to_mock.method_example.assert_called_once_with("parameter example")
22 |
23 | object_to_mock.method_example()
24 |
25 | assert object_to_mock.method_example.call_count == 2
26 |
27 | object_to_mock.method_example.assert_called_with()
28 |
29 |
30 | class TestRunDice(unittest.TestCase):
31 | """ Tests for the run_dice function isolating it from random library
32 | """
33 | # With @patch("testing.mocking.random"), we mock random from the mocking.py
34 | # file which is the file where the function run_dice is located.
35 | @patch("testing.mocking.random")
36 | # The mock_random parameter of the test_mock_function_with_dice method is
37 | # the name of the mock object we pass to the test method.
38 | def test_mock_function_with_dice(self, mock_random):
39 | """ Testing run_dice function
40 | """
41 | # We say randint should return 4 then we execute the run_dice function.
42 | mock_random.randint.return_value = 4
43 |
44 | # Testing if run_dice returns "Lucky".
45 | assert mocking.run_dice() == "Lucky"
46 |
47 | # We say that randint should return 3.
48 | mock_random.randint.return_value = 3
49 |
50 | # We execute the run_dice function and test if it returns "Unlucky".
51 | assert mocking.run_dice() == "Unlucky"
52 |
--------------------------------------------------------------------------------
/testing/pytesting.py:
--------------------------------------------------------------------------------
1 | """ Example code to show Pytest features """
2 |
3 |
4 | import uuid
5 | import dataclasses
6 | import sys
7 | from typing import Dict
8 |
9 |
10 | @dataclasses.dataclass
11 | class Product:
12 | """ Class Product example """
13 | id: uuid.UUID
14 | price: float
15 | name: str
16 |
17 |
18 | def add(number_1: int, number_2: int) -> int:
19 | """
20 | Add two numbers.
21 |
22 | :param number_1: First number.
23 | :param number_2: Second number.
24 | :return: Sum of two numbers.
25 | """
26 | return number_1 + number_2
27 |
28 |
29 | def get_user_inputs() -> Dict[str, str]:
30 | """ Get User Inputs
31 | :return: Dictionary with user inputs
32 | """
33 | received_user_input1 = str(input("What is your first input?"))
34 | received_user_input2 = str(input("What is your second input?"))
35 | return {
36 | "user_input1": received_user_input1,
37 | "user_input2": received_user_input2
38 | }
39 |
40 |
41 | def print_hello_world_with_error():
42 | """ Print Hello World with error """
43 | print("Hello World")
44 | sys.stderr.write("This is an error")
45 |
--------------------------------------------------------------------------------
/testing/pytesting_test.py:
--------------------------------------------------------------------------------
1 | """ Tests restated to Pytest """
2 |
3 |
4 | import pytest
5 | from .pytesting import add, get_user_inputs, print_hello_world_with_error
6 |
7 |
8 | def test_add():
9 | """ Doing simple tests """
10 | assert add(1, 2) == 3
11 | assert add(1, 2) < 4
12 |
13 |
14 | def test_product_list(product_fixture_list):
15 | """ Test example using fixture """
16 | assert len(product_fixture_list) == 2
17 | assert add(
18 | product_fixture_list[0].price,
19 | product_fixture_list[1].price
20 | ) == 12.11
21 |
22 |
23 | @pytest.mark.parametrize("parameter_1, parameter_2, expected_result", [
24 | (1, 2, 3),
25 | (-1, 1, 0),
26 | (0, 1, 1)])
27 | def test_add_parametrize(parameter_1, parameter_2, expected_result):
28 | """ Test example using parametrize feature of Pytest """
29 | assert add(parameter_1, parameter_2) == expected_result
30 |
31 |
32 | def test_stdout_stderr(capsys):
33 | """ Test example using capsys to test stdout and stderr """
34 | print_hello_world_with_error()
35 | captured = capsys.readouterr()
36 | # Asserting that the captured standard output (print) is what we expect
37 | # Notice that print adds a newline (/n) at the end of the string
38 | assert captured.out == "Hello World\n"
39 | # Asserting that the captured standard error (sys.stderr.write) is what we
40 | # expect
41 | assert captured.err == "This is an error"
42 |
43 |
44 | def test_user_input(monkeypatch):
45 | fake_user_input1 = "This is the first user input"
46 | fake_user_input2 = 42
47 | fake_user_inputs = iter([fake_user_input1, str(fake_user_input2)])
48 | # This simulates user inputs in the terminal, each input as one variable:
49 | monkeypatch.setattr('builtins.input', lambda _: next(fake_user_inputs))
50 | # Printing and generating error:
51 | received_user_inputs = get_user_inputs()
52 | # Asserting that the received user inputs are what we expect
53 | assert received_user_inputs["user_input1"] == fake_user_input1
54 | assert received_user_inputs["user_input2"] == str(fake_user_input2)
55 |
--------------------------------------------------------------------------------
/validation/validation.py:
--------------------------------------------------------------------------------
1 | """ Validator for the data
2 | """
3 |
4 |
5 | # It is necessary to ignore mypy Cerberus errors because it apparently misses
6 | # library stubs or py.typed marker
7 | from cerberus import Validator # type: ignore
8 |
9 | schema = {
10 | "name": {
11 | "type": "string",
12 | "minlength": 3,
13 | "maxlength": 80,
14 | "required": True,
15 | "empty": False
16 | }
17 | }
18 |
19 |
20 | validator = Validator(schema)
21 | data = {"name": "Jo"}
22 | if not validator.validate(data):
23 | print('Invalid Data:')
24 | print(validator.errors)
25 |
--------------------------------------------------------------------------------