Pytest маркировка тестов

Pytest маркировка тестов Маркировка
Содержание
  1. Start using pytest fixtures
  2. Run only our test
  3. Write robust unit tests with Python pytest
  4. Next steps
  5. Run the tests
  6. Run the tests (again)
  7. Check code coverage
  8. Generate an HTML coverage report
  9. Skip a single test
  10. Test requirements
  11. Write another test
  12. Test selection via marker expression
  13. Работа с пользовательскими маркерами¶
  14. Маркировка тестовых функций и выбор их для прогона¶
  15. Выбор тестов на основе их идентификатора узла¶
  16. Использование для выбора тестов на основе их названия¶
  17. Регистрация маркеров¶
  18. Отметка целых классов или модулей¶
  19. Маркировка отдельных тестов при использовании параметризации¶
  20. Пользовательский маркер и опция командной строки для управления тестовыми прогонами¶
  21. Передача вызываемого объекта пользовательским маркерам¶
  22. Чтение маркеров, которые были установлены из нескольких мест¶
  23. Автоматическое добавление маркеров на основе названий тестов¶
  24. Add marker documentation
  25. About this blog post
  26. Explore the project
  27. Specify fixture names
  28. Our task
  29. Skip entire test module
  30. Show slow running tests
  31. Write a happy path test
  32. Show output
  33. Setting Up Pytest
  34. Missing coverage
  35. Repeating tests
  36. Run the example

Start using pytest fixtures

Now that we know that the test passes, let’s refactor our code and separate out
test dependencies from the test implementation using pytest
fixtures and also remove the prints as they are not required for the
test.

PyBerlin is a new meetup community in Berlin, Germany that strives
to be open and inclusive, with talks from a broad range of topics for anyone
who is interested in Python.

The PyBerlin organizers invited me to give the opening talk at the first event,
which took place last month in February, and asked me to speak about automated
testing with pytest. I was super excited and I also felt very honored
to be the first speaker for the community. I wanted to prepare something
special! 👨🏻‍💻

New concepts stick much better with me when applied to a real problem rather
than when explained with a bunch of trivial examples. So instead of using a
slide deck packed with short code snippets to explain the core concepts of
writing tests in Python with pytest, I decided to do some live coding on earth,
a small example project, and work on a patch for an open GitHub issue.

earth is a library for organizing and running events: You can invite
adventurers from around the world, see how they pack their bags and travel by
airplane to the event location, and finally greet them and make introductions.
🏕

The GitHub issue reads:

Another version of this talk was live streamed on YouTube and
the recording is now available on our
Mozilla YouTube channel, if you would rather
watch my talk than read this rather lengthy blog post. 📺

Pytest маркировка тестов

Picture taken on Feb 21, 2019 by Tammo Behrends at PyBerlin 📷

Run only our test

We can now use the markers to select our test and see if it passes:

============================ test session starts =============================
collected 50 items / 49 deselected / 1 skipped

============= 1 passed, 1 skipped, 49 deselected in 0.11 seconds =============

Yay it works! 😄

Write robust unit tests with Python pytest

While the unittest package is object-oriented since test cases are written in classes, the pytest package is functional, resulting in fewer lines of code. Personally, I prefer unittest as I find the codes more readable. That being said, both packages, or rather frameworks, are equally powerful, and choosing between them is a matter of preference.

Update: This article is part of a series. Check out other “in 10 Minutes” topics here!

Next steps

We’ve completed our task of increasing the code coverage through automated
tests, but we have some more work to do! 🚧

Running all of the tests including the slow test isn’t great for developer
productivity. It makes sense to write a custom pytest plugin that skips tests
marked as slow by default and only includes slow tests if we run pytest with a
custom CLI option.

Also, did you notice that we have three test cases, but apart from the markers
and the different fixtures the test functions itself are identical? That’s a
lot of redundancy. 😔

The good news is that pytest comes with a marker which solves this exact
problem. Parametrized tests and custom plugins will be our topics in the next
part of this tutorial!

Update: Customizing your pytest test suite (part 2) is now online! 👨🏻‍💻

Run the tests

Let’s start with running the existing tests as they are:

E File «earth/tests/old/stuff/test_stuff_03.py», line 6
E print «hello world»
E ^
E SyntaxError: Missing parentheses in call to ‘print’. Did you mean

Run the tests (again)

Run pytest again and watch closely what happens:

You will find that we don’t see the error anymore, but the 4 tests in
tests/old/stuff/test_stuff_02.py take a fairly long time to complete. 😴

Check code coverage

Now let’s run all of our tests and check the current code coverage. 🌏

Name Stmts Miss Cover
earth/__init__.py 3 0 100%
earth/adventurers.py 71 0 100%
earth/events.py 24 0 100%
earth/travel.py 31 3 90%
earth/year.py 14 0 100%
TOTAL 143 3 98%

Yay, we’re at 98% code coverage for the earth project! That’s fantastic! 😁

Generate an HTML coverage report

It’s a good idea to check whether adding this test changed the code coverage.
This time we will run the tests again and generate an HTML coverage report to
find out not only the percentage of lines covered by tests, but also which
lines are not executed when we run the tests.

pytest —cov earth/ —cov-report html

This creates a new file at htmlcov/index.html 📄

When you open that coverage report in your web browser, you will see that our
test coverage increased quite drastically. It is now at 84% — woohoo! 😃

Skip a single test

Let’s comment out the invalid code, add a comment so that we don’t forget to
look into this later and skip the test.

# TODO: Can we remove this test?
# print «hello world»

Test requirements

We can use the built-in xfail marker from pytest for our flaky test:
Tests marked with xfail are expected to fail due to the specified reason. If
they fail, they will be reported as XFAIL, however if they unexpectedly
pass they will be reported as XPASS.

# Bears in warm climates don’t hibernate 🐻

# Bears in warm climates don’t hibernate 🐻

Run the tests again:

pytest -v -m «wip and not slow» —count 10

============================ test session starts =============================

= 10 passed, 1 skipped, 500 deselected, 8 xfailed, 2 xpassed in 0.24 seconds =

Write another test

We have one test that does not use all of the adventurers.py functions, but
is really fast. We have one test that uses all of the adventurers.py
functions, but is rather slow. The ideal test would be one that uses as many
adventurers.py functions as possible and is really fast. 💨

Let’s write a new pytest fixture no_pandas_group and another test named
test_no_pandas_group:

Test selection via marker expression

We now have three different tests and added custom markers to them.
Let’s run all wip tests that are not slow. 💻

pytest -m «wip and not slow»

============================ test session starts =============================
collected 52 items / 50 deselected / 1 skipped / 1 selected

============= 2 passed, 1 skipped, 50 deselected in 0.19 seconds =============

Работа с пользовательскими маркерами¶

Вот несколько примеров с использованием механизма Как пометить тестовые функции атрибутами.

Маркировка тестовых функций и выбор их для прогона¶

Вы можете «пометить» тестовую функцию пользовательскими метаданными следующим образом:

# content of test_server.py

# perform some webtest test for your app

Затем можно ограничить выполнение теста, чтобы запускать только тесты, помеченные :

$ pytest -v -m webtest
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y — $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 4 items / 3 deselected / 1 selected

test_server.py::test_send_http

, in 0.12s ======================

Или наоборот, запустить все тесты, кроме webtest:

$ pytest -v -m «not webtest»
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y — $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 4 items / 1 deselected / 3 selected

test_server.py::test_something_quick
test_server.py::test_another
test_server.py::TestClass::test_method

, in 0.12s ======================

Выбор тестов на основе их идентификатора узла¶

Вы можете указать один или несколько в качестве позиционных аргументов, чтобы выбрать только указанные тесты. Это упрощает выборку тестов на основе имени их модуля, класса, метода или функции:

$ pytest -v test_server.py::TestClass::test_method
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y — $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 1 item

test_server.py::TestClass::test_method

in 0.12s =============================

Вы также можете выбрать класс:

$ pytest -v test_server.py::TestClass
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y — $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 1 item

test_server.py::TestClass::test_method

in 0.12s =============================

Или выберите несколько узлов:

$ pytest -v test_server.py::TestClass test_server.py::test_send_http
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y — $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 2 items

test_server.py::TestClass::test_method
test_server.py::test_send_http

in 0.12s =============================

Идентификаторы узлов для неудачных тестов отображаются в сводной информации о тесте при запуске pytest с опцией . Вы также можете построить идентификаторы узлов из вывода .

Использование для выбора тестов на основе их названия¶

Добавлено в версии 2.0/2.3.4.

Вы можете использовать опцию командной строки , чтобы указать выражение, которое реализует подстрочное соответствие имен тестов вместо точного соответствия маркеров, которое обеспечивает . Это упрощает выборку тестов на основе их имен:

Изменено в версии 5.4.

Сопоставление выражений теперь не чувствительно к регистру.

$ pytest -v -k http # running with the above defined example module
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y — $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 4 items / 3 deselected / 1 selected

test_server.py::test_send_http

, in 0.12s ======================

Также можно запустить все тесты, кроме тех, которые соответствуют ключевому слову:

$ pytest -k «not send_http» -v
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y — $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 4 items / 1 deselected / 3 selected

test_server.py::test_something_quick
test_server.py::test_another
test_server.py::TestClass::test_method

, in 0.12s ======================

Или выбрать «http» и «быстрый» тесты:

$ pytest -k «http or quick» -v
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y — $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 4 items / 2 deselected / 2 selected

test_server.py::test_send_http
test_server.py::test_something_quick

, in 0.12s ======================

Вы можете использовать , , и круглые скобки.

Помимо имени теста, также соответствует именам родителей теста (обычно это имя файла и класса, в котором он находится), атрибутам, установленным для функции теста, маркерам, примененным к нему или его родителям, и любым , явно добавленным к нему или его родителям.

Регистрация маркеров¶

Регистрация маркеров для вашего набора тестов проста:

# content of pytest.ini

webtest: mark a test as a webtest.
slow: mark test as slow.

Можно зарегистрировать несколько пользовательских маркеров, определив каждый из них в отдельной строке, как показано в примере выше.

Вы можете спросить, какие маркеры существуют для вашего набора тестов — список включает только что определенные маркеры и :

Пример добавления и работы с маркерами из плагина смотрите в Пользовательский маркер и опция командной строки для управления тестовыми прогонами.

Рекомендуется явно регистрировать маркеры, чтобы:

Отметка целых классов или модулей¶

Вы можете использовать декораторы с классами, чтобы применить маркеры ко всем его тестовым методам:

# content of test_mark_classlevel.py

Это эквивалентно прямому применению декоратора к двум тестовым функциям.

Чтобы применить метки на уровне модуля, используйте глобальную переменную :

или несколько маркеров:

По унаследованным причинам, до введения декораторов классов, можно установить атрибут для тестового класса следующим образом:

Маркировка отдельных тестов при использовании параметризации¶

При использовании параметризации применение метки сделает ее применимой к каждому отдельному тесту. Однако можно также применить маркер к отдельному экземпляру теста:

В этом примере метка «foo» будет применена к каждому из трех тестов, тогда как метка «bar» будет применена только ко второму тесту. Таким же образом можно применять метки «пропуск» и «xfail», см. раздел Пропуск/xfail с параметризацией.

Пользовательский маркер и опция командной строки для управления тестовыми прогонами¶

Плагины могут предоставлять пользовательские маркеры и реализовывать специфическое поведение на их основе. Это самодостаточный пример, который добавляет опцию командной строки и маркер параметризованной тестовой функции для запуска тестов, указанных через именованные окружения:

# content of conftest.py

«only run tests matching the environment NAME.»

# register an additional marker

«env(name): mark test to run only on named environment»

«test requires env in

Тестовый файл, использующий этот локальный плагин:

# content of test_someenv.py

и пример вызова с указанием среды, отличной от той, которая нужна тесту:

$ pytest -E stage2
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_someenv.py

in 0.12s ============================

и вот один, который точно определяет необходимую среду:

$ pytest -E stage1
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_someenv.py

in 0.12s =============================

Опция всегда выдает список доступных маркеров:

Передача вызываемого объекта пользовательским маркерам¶

Ниже приведен файл конфигурации, который будет использоваться в следующих примерах:

Пользовательский маркер может иметь свой набор аргументов, то есть свойства и , определяемые либо вызовом его как вызываемого, либо с помощью . В большинстве случаев эти два метода достигают одного и того же эффекта.

Однако, если в качестве единственного позиционного аргумента имеется вызываемый объект без аргументов в виде ключевых слов, использование не передаст в качестве позиционного аргумента, а украсит пользовательским маркером (см. ). К счастью, на помощь приходит :

# content of test_custom_marker.py

На выходе получаем следующее:

Мы видим, что у пользовательского маркера набор аргументов расширен функцией . Это ключевое различие между созданием пользовательского маркера как вызываемой функции, которая вызывает за кулисами, и использованием .

Чтение маркеров, которые были установлены из нескольких мест¶

Если вы активно используете маркеры в своем тестовом наборе, вы можете столкнуться со случаем, когда маркер применяется к тестовой функции несколько раз. Из кода плагина вы можете прочитать все такие настройки. Пример:

# content of test_mark_three_times.py

Здесь мы имеем маркер «glob», примененный три раза к одной и той же тестовой функции. Из файла conftest мы можем прочитать это следующим образом:

Давайте запустим его без захвата вывода и посмотрим, что мы получим:

Предположим, у вас есть набор тестов, в котором тесты помечены для определенных платформ, а именно , и т.д., и у вас также есть тесты, которые выполняются на всех платформах и не имеют конкретного маркера. Если вы хотите иметь возможность запускать только тесты для вашей конкретной платформы, вы можете использовать следующий плагин:

# content of conftest.py

«darwin linux win32»

«cannot run on platform

то тесты будут пропущены, если они были заданы для другой платформы. Давайте сделаем небольшой тестовый файл, чтобы показать, как это выглядит:

# content of test_plat.py

то вы увидите два пропущенных теста и два выполненных, как и ожидалось:

Обратите внимание, что если вы укажете платформу через опцию командной строки marker-command line следующим образом:

$ pytest -m linux
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items / 3 deselected / 1 selected

test_plat.py

, in 0.12s ======================

то немаркированные тесты выполняться не будут. Таким образом, это способ ограничить выполнение только определенных тестов.

Автоматическое добавление маркеров на основе названий тестов¶

Если у вас есть набор тестов, в котором имена тестовых функций указывают на определенный тип теста, вы можете реализовать хук, который автоматически определяет маркеры, чтобы вы могли использовать с ним опцию . Давайте рассмотрим этот тестовый модуль:

# content of test_module.py

Мы хотим динамически определить два маркера и можем сделать это в плагине :

Теперь мы можем использовать для выбора одного набора:

$ pytest -m interface —tb=short
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items / 2 deselected / 2 selected

test_module.py

================================= FAILURES =================================
__________________________ test_interface_simple ___________________________
:4: in test_interface_simple
assert 0
E assert 0
__________________________ test_interface_complex __________________________
:8: in test_interface_complex
assert 0
E assert 0
========================= short test summary info ==========================
test_module.py:: — assert 0
test_module.py:: — assert 0
, in 0.12s ======================

или выбрать оба теста «событие» и «интерфейс»:

$ pytest -m «interface or event» —tb=short
=========================== test session starts ============================
platform linux — Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items / 1 deselected / 3 selected

test_module.py

================================= FAILURES =================================
__________________________ test_interface_simple ___________________________
:4: in test_interface_simple
assert 0
E assert 0
__________________________ test_interface_complex __________________________
:8: in test_interface_complex
assert 0
E assert 0
____________________________ test_event_simple _____________________________
:12: in test_event_simple
assert 0
E assert 0
========================= short test summary info ==========================
test_module.py:: — assert 0
test_module.py:: — assert 0
test_module.py:: — assert 0
, in 0.12s ======================

Add marker documentation

Let’s add a new custom pytest marker slow to test_large_group. It’s
good practice to write documentation for custom markers and provide a short
description for every marker, that explains the characteristics of tests with
that marker. 📝

Add a pytest configuration file with a markers section:

slow: tests that take a long time to complete.

Now if you run pytest —markers you will see the information about pytest’s
markers and also our custom one.

About this blog post

I’ve structured this blog post as a tutorial. 📝

Good tutorials are difficult to write. I’ll try my best to give you enough
information to complete the steps without going into too much detail. If you
spot any typos, find bugs in the code examples, or have any questions, please
open a new issue on GitHub. 💻

I will commit code changes to the earth repository on a separate branch named
increase-test-coverage and create commits for the steps in this
tutorial. Should you get stuck at any point, please check out the commits on
that branch and continue from there.

Let’s get started! 😄

Explore the project

There are a number of tests in the tests directory of the earth project. Have a
look around and open a few files in your text editor to make yourself familiar
with what the tests are doing. 🕵️‍♀️

│   ├── __init__.py
│   ├── test_adventurers_01.py
│   ├── test_adventurers_02.py
│   └── test_adventurers_03.py
│   ├── __init__.py
│   ├── test_earth_01.py
│   └── test_earth_02.py
│   ├── __init__.py
│   ├── test_events_01.py
│   ├── test_events_02.py
│   ├── test_events_03.py
│   └── test_events_04.py
│   ├── __init__.py
│   └── stuff
│   ├── __init__.py
│   ├── test_stuff_01.py
│   ├── test_stuff_02.py
│   └── test_stuff_03.py
│   ├── __init__.py
│   ├── test_travel_01.py
│   ├── test_travel_02.py
│   └── test_travel_03.py

7 directories, 25 files

You will find that only tests/year/test_year_02.py imports the earth
library, which indicates that the majority of the existing tests do not use the
earth library and can’t possibly generate code coverage.

We’ll leave the test for the example from the README as is and write a new test
for a larger group of adventurers. Rename the fixture friends to
small_group and rename the test from test_earth to
test_small_group.

Create a new fixture large_group based on small_group but return a
complete list of adventurers and copy test_small_group to a new
test_large_group test.

Specify fixture names

We forgot to rename small_group in the for loop in test_large_group
🤦‍♂

However because we define a function with name small_group in the module
scope and everything in Python is an object, Python tries to iterate over our
fixture function instead of raising a NameError.

I recommend overwriting the auto-generated fixture name to avoid this problem.

Now if we run the tests again, we will see a much more helpful error message. I
can’t stress enough how important this is. With Python’s
duck-typing capabilities, you might not even realize that your
tests accidentally pass only because you forgot to add your fixture to your
test’s argument list. ⚠️

Let’s fix that and update the variable name in test_large_group, before we
run the tests again. 💻

Our task

We will work on identifying blind spots in the earth project and increasing its
code coverage by developing a series of automated tests. 🌏

We need a local copy of the earth GitHub repository as well
as a new Python 3.7 virtual environment with attrs installed.
🐍

Skip entire test module

Let’s generate a test coverage report using pytest-cov to see if
the test module with the slow tests generates test coverage:

Name Stmts Miss Cover
earth/__init__.py 3 0 100%
earth/adventurers.py 71 41 42%
earth/events.py 24 12 50%
earth/travel.py 31 14 55%
earth/year.py 14 0 100%
TOTAL 143 67 53%

Then skip the entire test module:

«This does not generate code coverage»

# check that s.split fails when the separator is not a string

Let’s generate a new test coverage report. 💻

You will see that the test file was skipped and that the test coverage remained
at 53%. This means that the test does not generate any code coverage and is
safe to skip.

Show slow running tests

If you run the tests, you will notice that the suite now takes a long time to
complete. pytest comes with a really neat CLI flag to run the tests and show a
sorted list of slow tests. Let’s show only the top 2 slowest tests ⏱

pytest -m wip —durations 2

The new test eventually failed after taking more than 20 seconds to complete. ⏱

While the —durations CLI flag might not be super helpful with only two
tests and we already knew that the first new test passed in less than one
second in a previous run, it’s super useful for larger test suites.

Write a happy path test

Let’s create a new test file and write a test based on the example. Note that
we won’t perform any assertions in our test, but only test for unhandled
exceptions.

I also recommend to add custom pytest markers to tests that we
currently work on as it makes selecting them that much easier. We’ll use
happy because our new test will be a happy path test and wip to
indicate that this test is a work in progress.

«Hello adventurers! 🏕»

Let’s run our work in progress tests again and see if the new test passes:

Wait, what? Our test fails with a TypeError?! 🤔

Show output

We know which test is slow, but we don’t really know why that is. By default
pytest captures any output sent to stdout and stderr. Let’s run the
slow test again with capturing disabled, so that we see prints while the test
runs.

Random fact from the nature documentary Earth: One Amazing
Day 😂

Pandas are very fussy when it comes to their diet. Bamboo is almost the only
thing they eat. But because bamboo is not very nutritious, pandas need to eat
up to 14 hours a day to get enough energy to feed themselves. 🐼

Setting Up Pytest

Unlike unittest, pytest is not a built-in Python package and requires installation. This can simply be done with pip install pytest on the Terminal.

Do note that the best practices for unittest also apply for pytest, such that

Missing coverage

The HTML coverage report also shows code coverage for individual files. We can
use this information to work on increasing the code coverage for earth even
more. The first file in our report adventurers.py has a coverage of 80%. If
you click on the link in the HTML, you will see a number of red lines, which
means we don’t have coverage for them.

Let’s do something about that! 🐼🐻🦊

Repeating tests

Remember in our previous test run the slow test failed due to an unhandled
exception. According to the error our fox adventurer “Dave” didn’t make it to
PyCon. 🦊

earth.events.MissingAttendee: Oh no! Dave is not here! 😟

It’s rather suspicious that the test_no_pandas_group passed, when the only
difference to the test_large_group test is that it doesn’t have any pandas.
This might be because tests with foxes are flaky and only fail under certain
conditions.

We can check this by running our tests multiple times with
pytest-repeat. Let’s run our fast, wip tests 10 times.

============================ test session starts =============================

======= 9 failed, 11 passed, 1 skipped, 500 deselected in 0.30 seconds =======

Run the example

The README file in the earth repo contains an example for how the library can
be used and the very same example is also copied to the example1.py script.

Let’s run the example and see what it does:

Hello adventurers! 🏕
Bruno accepted our invite! 😃
Michael accepted our invite! 😃
Brianna accepted our invite! 😃
Julia accepted our invite! 😃
🐸 Bruno is packing 👜
🐸 Bruno is travelling: South America ✈️ North America
🦁 Michael is packing 👜
🦁 Michael is travelling: Africa ✈️ North America
🐨 Brianna is packing 👜
🐨 Brianna is travelling: Australia ✈️ North America
🐯 Julia is travelling: Asia ✈️ North America
Welcome to PyCon US in North America! 🎉
🐸 Hello, my name is Bruno!
🦁 Hello, my name is Michael!
🐨 Hello, my name is Brianna!
🐯 Hello, my name is Julia!

Оцените статью
Маркировка-Про