Skip to content
Merged

uv #196

Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/devRun.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: psf/black@stable
- uses: isort/isort-action@v1
- name: Set up Python
uses: actions/setup-python@v5
with:
Expand All @@ -23,7 +21,7 @@ jobs:
- name: Create venv & install dependencies
run: |
uv venv
uv pip sync uv.lock
uv sync --all-extras --dev
- name: Install Playwright Browsers
run: |
PLAYWRIGHT_VERSION=$(grep -E '^playwright = "[^"]*"' pyproject.toml | sed -E 's/playwright = "([^"]*)".*$/\1/')
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
- name: Create venv & install dependencies
run: |
uv venv
uv pip sync uv.lock
uv sync --all-extras --dev
- name: Install Playwright Browsers
run: |
PLAYWRIGHT_VERSION=$(grep -E '^playwright = "[^"]*"' pyproject.toml | sed -E 's/playwright = "([^"]*)".*$/\1/')
Expand Down
40 changes: 10 additions & 30 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,14 @@ repos:
- id: conventional-pre-commit
stages: [commit-msg]
args: []
- repo: https://github.com/psf/black
rev: 25.1.0
hooks:
- id: black
language_version: python3
args: [ '--config', 'pyproject.toml' ]
- repo: https://github.com/PyCQA/autoflake
rev: v2.3.1
hooks:
- id: autoflake
args:
[
'--in-place',
'--remove-unused-variable',
'--remove-all-unused-imports',
'--expand-star-imports',
'--ignore-init-module-imports',
]
- repo: https://github.com/PyCQA/isort
rev: 6.0.1
hooks:
- id: isort
args: [ '--settings-file', 'pyproject.toml' ]
- repo: https://github.com/asottile/pyupgrade
rev: v3.19.1
hooks:
- id: pyupgrade
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.4
hooks:
- id: ruff
args: [ --fix ]
continue_on_error: true
- id: ruff-format
continue_on_error: true
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
Expand All @@ -72,11 +53,10 @@ repos:
rev: v0.24.1
hooks:
- id: validate-pyproject
# Optional extra validations from SchemaStore:
additional_dependencies: [ "validate-pyproject-schema-store[all]" ]
additional_dependencies: ["validate-pyproject-schema-store[all]"]
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.14.0
hooks:
- id: pretty-format-toml
exclude: poetry.lock
args: [ --autofix ]
args: [--autofix]
10 changes: 5 additions & 5 deletions .github/README.md → README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
![YouTube Channel](https://img.shields.io/youtube/channel/subscribers/UCQjS-eoKl0a1nuP_dvpLsjQ?label=YouTube%20Channel)
![dev run](https://github.com/nirtal85/Playwright-Python-Example/actions/workflows/devRun.yml/badge.svg)
![nightly](https://github.com/nirtal85/Playwright-Python-Example/actions/workflows/nightly.yml/badge.svg)
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)

## 📃 Articles written about this project

Expand Down Expand Up @@ -50,15 +50,15 @@ cd playwright-python
pip install uv
uv venv
.\env\Scripts\activate
uv pip sync uv.lock
uv sync --all-extras --dev
```

#### For Mac:
```bash
python3 -m pip install uv
uv venv
source .venv/bin/activate
uv pip sync uv.lock
source .venv/bin/activate
uv sync --all-extras --dev
```

### Install playwright
Expand Down
3 changes: 1 addition & 2 deletions pages/login_page.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from dataclasses import dataclass
from typing import Union

import allure
from playwright.sync_api import Page
Expand All @@ -19,7 +18,7 @@ def __init__(self, page: Page):
self.error_message = page.get_by_test_id("error")

@allure.step("Login with username {username} and password {password}")
def login(self, username: Union[User, str], password: str):
def login(self, username: User | str, password: str):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix type annotation causing pipeline failure

The pipeline is failing with: TypeError: unsupported operand type(s) for |: 'module' and 'type'. This indicates either:

  1. The Python version in your environment doesn't support the pipe operator for unions (needs Python 3.10+), or
  2. There's an issue with how User is imported

Looking at the imports, there's a mismatch: you import with from enums import User but the enum is defined in enums/User.py.

Choose one of these options to fix it:

# Option 1: Revert to using typing.Union if you need Python <3.10 compatibility
- def login(self, username: User | str, password: str):
+ from typing import Union
+ def login(self, username: Union[User, str], password: str):
# Option 2: Fix the import if User is a class in enums/User.py
- from enums import User
+ from enums.User import User

Additionally, consider addressing these static analysis recommendations:

  • Add a return type annotation: -> None
  • Add a docstring describing what the method does
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def login(self, username: User | str, password: str):
# Before (original import and function definition)
# from enums import User
# ...
# def login(self, username: User | str, password: str):
# After (updated import and function definition)
from enums.User import User
class LoginPage:
# ... other methods and class content ...
def login(self, username: User | str, password: str) -> None:
"""Logs in a user with the provided username and password.
Args:
username (User | str): A User instance or username as a string.
password (str): The user's password.
"""
# Implementation goes here
🧰 Tools
🪛 Ruff (0.8.2)

21-21: Missing return type annotation for public function login

Add return type annotation: None

(ANN201)


21-21: Missing docstring in public method

(D102)

🪛 GitHub Actions: Pre merge test

[error] 21-21: TypeError: unsupported operand type(s) for |: 'module' and 'type'

if hasattr(username, "value"):
self.user_name_field.fill(username.value)
else:
Expand Down
76 changes: 45 additions & 31 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,48 +1,47 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "playwright-python"
version = "0.1.0"
description = "Playwright Python example project with pytest and Allure report"
authors = [{ name = "nirtal85", email = "nirt236@gmail.com" }]
requires-python = "~=3.11"
readme = "README.md"
keywords = [
"playwright",
"automation",
"testing",
"web",
]
dependencies = [
"allure-pytest==2.13.5",
"axe-playwright-python==0.1.4",
"playwright==1.51.0",
"pytest==8.3.5",
"pytest-base-url==2.1.0",
"pytest-playwright==0.7.0",
"pytest-split==0.10.0",
"requests==2.32.3",
]
requires = ["hatchling"]

[dependency-groups]
dev = [
"black==25.1.0",
"isort==6.0.1",
"pre-commit==4.2.0",
"ruff==0.11.4",
"pre-commit==4.2.0"
]

[tool.isort]
profile = "black"
skip = ["env", "venv"]
[project]
authors = [{name = "nirtal85", email = "nirt236@gmail.com"}]
dependencies = [
"allure-pytest==2.13.5",
"axe-playwright-python==0.1.4",
"playwright==1.51.0",
"pytest==8.3.5",
"pytest-base-url==2.1.0",
"pytest-playwright==0.7.0",
"pytest-split==0.10.0",
"requests==2.32.3"
]
description = "Playwright Python example project with pytest and Allure report"
keywords = [
"playwright",
"automation",
"testing",
"web"
]
name = "playwright-python"
readme = "README.md"
requires-python = "~=3.11"
version = "0.1.0"

[tool.hatch.build.targets.sdist]
include = ["playwright_python"]

[tool.hatch.build.targets.wheel]
include = ["playwright_python"]

[tool.isort]
profile = "black"
skip = ["env", "venv"]

[tool.pytest.ini_options]
addopts = [
"--clean-alluredir",
Expand All @@ -68,3 +67,18 @@ log_cli_level = "INFO"
markers = [
"devRun: marks tests that run before merge to the main branch"
]

[tool.ruff]
exclude = [".venv", "env"]
ignore = [
"D203", # One blank line required before class docstring (conflicts with D211)
"D213", # Multi-line docstring summary should start at the second line
"COM812"
]
line-length = 100
select = ["ALL"]
target-version = "py311"

[tool.ruff.format]
docstring-code-format = true
quote-style = "double"
1 change: 0 additions & 1 deletion tests/accesability_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@


class TestAccessibility:

@allure.title("Test Accessibility with Default Counts")
def test_accessibility_default_counts(self, axe_playwright, page):
axe_playwright.check_accessibility(page)
Expand Down
5 changes: 1 addition & 4 deletions tests/checkout_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@


class TestCheckout:

@pytest.mark.parametrize(
"browser_context_args", [User.STANDARD_USER], indirect=True
)
@pytest.mark.parametrize("browser_context_args", [User.STANDARD_USER], indirect=True)
def test_checkout_counter(self, browser_context_args, page: Page):
page.evaluate("localStorage.setItem('cart-contents', '[4,0]');")
page.reload()
Expand Down
19 changes: 12 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Dict

import allure
import pytest
import requests
Expand Down Expand Up @@ -27,6 +25,7 @@ def goto(page: Page, request: SubRequest):

Example:
@pytest.mark.parametrize('browser_context_args', ["standard_user"], indirect=True)

"""
if request.getfixturevalue("browser_context_args").get("storage_state"):
page.goto("/inventory.html")
Expand All @@ -43,27 +42,29 @@ def axe_playwright():

Returns:
AxeHelper: An instance of AxeHelper with Axe initialized.

"""
yield AxeHelper(Axe())
return AxeHelper(Axe())


@pytest.fixture(scope="function")
def browser_context_args(
browser_context_args: Dict, base_url: str, request: SubRequest
):
def browser_context_args(browser_context_args: dict, base_url: str, request: SubRequest):
"""This fixture allows setting browser context arguments for Playwright.

Args:
browser_context_args (dict): Base browser context arguments.
request (SubRequest): Pytest request object to get the 'browser_context_args' fixture value.
base_url (str): The base URL for the application under test.

Returns:
dict: Updated browser context arguments.

See Also:
https://playwright.dev/python/docs/api/class-browser#browser-new-contex

Returns:
dict: Updated browser context arguments.

"""
context_args = {
**browser_context_args,
Expand All @@ -85,7 +86,7 @@ def browser_context_args(


@pytest.fixture(scope="session")
def browser_type_launch_args(browser_type_launch_args: Dict, playwright: Playwright):
def browser_type_launch_args(browser_type_launch_args: dict, playwright: Playwright):
"""Fixture to set browser launch arguments.

This fixture updates the browser launch arguments to start the browser maximized
Expand All @@ -103,6 +104,7 @@ def browser_type_launch_args(browser_type_launch_args: Dict, playwright: Playwri

See Also:
https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch

"""
playwright.selectors.set_test_id_attribute("data-test")
return {**browser_type_launch_args, "args": ["--start-maximized"]}
Expand All @@ -113,6 +115,7 @@ def get_public_ip() -> str:

Returns:
str: Public IP address.

"""
return requests.get(
"http://checkip.amazonaws.com",
Expand All @@ -129,6 +132,7 @@ def attach_playwright_results(page: Page, request: FixtureRequest):
Args:
page (Page): Playwright page object.
request: Pytest request object.

"""
yield
if request.node.rep_call.failed:
Expand Down Expand Up @@ -158,6 +162,7 @@ def pytest_runtest_makereport(item: Item):

Yields:
Outcome of the test execution.

"""
outcome = yield
rep = outcome.get_result()
Expand Down
5 changes: 1 addition & 4 deletions tests/inventory_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@


class TestInventory:

@pytest.mark.parametrize(
"browser_context_args", [User.STANDARD_USER], indirect=True
)
@pytest.mark.parametrize("browser_context_args", [User.STANDARD_USER], indirect=True)
def test_inventory_page(self, browser_context_args, page: Page):
expect(page.get_by_test_id("title")).to_have_text("Products")
4 changes: 1 addition & 3 deletions tests/login_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ def test_valid_login(self, base_url, page: Page):
ids=["invalid_password", "locked_user"],
)
@allure.title("Login with invalid credentials test")
def test_login_error(
self, page: Page, username: str, password: str, expected_error: str
):
def test_login_error(self, page: Page, username: str, password: str, expected_error: str):
self.login_page.login(username, password)
expect(self.login_page.error_message).to_have_text(expected_error)
11 changes: 3 additions & 8 deletions utilities/axe_helper.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import json
from collections import Counter
from typing import Dict

import allure
from axe_playwright_python.sync_playwright import Axe
from playwright.sync_api import Page


class AxeHelper:

def __init__(self, axe: Axe):
self.axe = axe

def check_accessibility(
self, page: Page, maximum_allowed_violations_by_impact: Dict[str, int] = None
self, page: Page, maximum_allowed_violations_by_impact: dict[str, int] = None
Comment on lines 13 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improved type hinting with modern syntax

The change from Dict[str, int] to dict[str, int] is appropriate and follows modern Python typing practices (Python 3.9+) where built-in container types can be used directly as type annotations.

However, there's a static analysis issue to fix:

- def check_accessibility(
-     self, page: Page, maximum_allowed_violations_by_impact: dict[str, int] = None
+ def check_accessibility(
+     self, page: Page, maximum_allowed_violations_by_impact: dict[str, int] | None = None

This change addresses the warning about implicit Optional types and makes the parameter typing more explicit.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def check_accessibility(
self, page: Page, maximum_allowed_violations_by_impact: Dict[str, int] = None
self, page: Page, maximum_allowed_violations_by_impact: dict[str, int] = None
def check_accessibility(
self, page: Page, maximum_allowed_violations_by_impact: dict[str, int] | None = None
🧰 Tools
🪛 Ruff (0.8.2)

14-14: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

) -> None:
"""Checks accessibility of the page using playwright axe.

Expand All @@ -33,15 +31,12 @@ def check_accessibility(
}
results = self.axe.run(page)
violations_count = dict(
Counter(
[violation["impact"] for violation in results.response["violations"]]
)
Counter([violation["impact"] for violation in results.response["violations"]])
)
if violations_exceeded := {
impact_level: violation_count
for impact_level, violation_count in violations_count.items()
if violation_count
> maximum_allowed_violations_by_impact.get(impact_level, 0)
if violation_count > maximum_allowed_violations_by_impact.get(impact_level, 0)
}:
allure.attach(
body=json.dumps(results.response["violations"], indent=4),
Expand Down
Loading
Loading