Kaynağa Gözat

chore: Stricter pre-commit checks

Sean 2 yıl önce
ebeveyn
işleme
f2bfd72fdd
6 değiştirilmiş dosya ile 210 ekleme ve 88 silme
  1. 0 17
      .mypy.ini
  2. 48 37
      .pre-commit-config.yaml
  3. 38 0
      cspell.config.yaml
  4. 9 3
      ocma/cli.py
  5. 34 26
      ocma/connect.py
  6. 81 5
      pyproject.toml

+ 0 - 17
.mypy.ini

@@ -1,17 +0,0 @@
-[mypy]
-
-
-[mypy-requests.*]
-ignore_missing_imports = True
-
-[mypy-yaml.*]
-ignore_missing_imports = True
-
-[mypy-toml.*]
-ignore_missing_imports = True
-
-[mypy-discord_webhook.*]
-ignore_missing_imports = True
-
-[mypy-otppy.*]
-ignore_missing_imports = True

+ 48 - 37
.pre-commit-config.yaml

@@ -1,5 +1,8 @@
 exclude: ^(data/.*|.vscode/.*|poetry.lock)$
 
+default_language_version:
+  python: python3.11
+
 repos:
   - repo: https://github.com/commitizen-tools/commitizen
     # Please run `pre-commit install --hook-type commit-msg` to check commit messages
@@ -21,55 +24,47 @@ repos:
       - id: check-added-large-files
         args: ["--maxkb=1000"]
 
-  - repo: https://github.com/codespell-project/codespell.git
-    rev: v2.2.6
+  - repo: https://github.com/streetsidesoftware/cspell-cli.git
+    rev: v8.1.1
+    hooks:
+      - id: cspell
+        name: cspell
+        additional_dependencies:
+          - "@cspell/dict-de-ch"
+          - "@cspell/dict-en-gb"
+        args:
+          - "lint"
+          - "--show-suggestions"
+          - "--no-must-find-files"
+          - "--locale"
+          - "en-GB,de-CH"
+
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.1.5
+    hooks:
+      - id: ruff
+        types_or: [python, pyi, jupyter]
+        args: ["--fix"]
+
+  # to be replaced by https://github.com/astral-sh/ruff/issues/458
+  - repo: https://github.com/jsh9/pydoclint
+    rev: 0.3.8
     hooks:
-      - id: codespell
-        name: codespell
+      - id: pydoclint
 
   - repo: https://github.com/asottile/pyupgrade
-    rev: v3.15.0
+    rev: v3.3.1
     hooks:
       - id: pyupgrade
-        args: [--py39-plus]
+        args: [--py311-plus]
 
   - repo: https://github.com/hadialqattan/pycln
-    rev: v2.4.0
+    rev: v2.1.3
     hooks:
       - id: pycln
         name: pycln (python)
         args: [--config=pyproject.toml]
 
-  - repo: https://github.com/psf/black
-    rev: 23.12.1
-    hooks:
-      - id: black
-
-  - repo: https://github.com/pycqa/isort
-    rev: 5.13.2
-    hooks:
-      - id: isort
-        name: isort (python)
-
-  - repo: https://github.com/PyCQA/pydocstyle
-    rev: 6.3.0
-    hooks:
-      - id: pydocstyle
-        name: pydocstyle
-
-  - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.8.0
-    hooks:
-      - id: mypy
-        name: mypy
-        additional_dependencies: ["types-pytz"]
-
-  - repo: https://github.com/pycqa/pylint
-    rev: v3.0.3
-    hooks:
-      - id: pylint
-        types: [python]
-
   - repo: local
     hooks:
       - id: poetry-install
@@ -80,6 +75,22 @@ repos:
         types: [python]
         pass_filenames: false
         stages: [commit, merge-commit, push, manual]
+      - id: mypy
+        name: Run mypy
+        entry: poetry run mypy ocma
+        language: system
+        always_run: false
+        types: [python]
+        pass_filenames: false
+        stages: [commit, merge-commit, push, manual]
+      - id: pylint
+        name: Run pylint
+        entry: poetry run pylint -j 0 ocma
+        language: system
+        always_run: false
+        types: [python]
+        pass_filenames: false
+        stages: [commit, merge-commit, push, manual]
       - id: run-tests
         name: Run tests
         entry: poetry run pytest

+ 38 - 0
cspell.config.yaml

@@ -0,0 +1,38 @@
+version: "0.2"
+allowCompoundWords: true
+ignorePaths:
+  - .pre-commit-config.yaml
+  - pyproject.toml
+  - .gitlab-ci.yml
+  - cspell.config.yaml
+  - .vscode/*
+  - .mypy.ini
+  - .gitignore
+  - .releaserc
+  - CHANGELOG.md
+  - .drone.yml
+dictionaryDefinitions: []
+ignoreWords:
+  - cifs
+  - xvzf
+  - pyproject
+  - ansi
+  - ocma
+  - otppy
+  - loginfmt
+  - SAOTCC
+  - OCMA
+  - fromb
+  - TOTP
+  - totp
+  - otpauth
+  - FHNW
+  - fhnw
+  - NBSWY3DPEB3W64TMMQ
+  - idDiv_SAOTCAS_Title
+  - Aelon
+dictionaries: []
+words: []
+import:
+  - "@cspell/dict-de-ch/cspell-ext.json"
+  - "@cspell/dict-en-gb/cspell-ext.json"

+ 9 - 3
ocma/cli.py

@@ -1,13 +1,19 @@
 """CLI interface."""
 
 import argparse
-from typing import Optional
 
 from ocma import connect
 
 
 def run() -> None:
-    """Run the CLI interface."""
+    """
+    Run the CLI interface.
+
+    Raises
+    ------
+    ValueError
+        If the MFS secret is invalid
+    """
     parser = argparse.ArgumentParser(description="openconnect-microsoft-authenticator")
 
     # add argument
@@ -68,7 +74,7 @@ def run() -> None:
     args = parser.parse_args()
 
     username: str = args.username
-    password: Optional[str] = args.password
+    password: str | None = args.password
     mfa_secret: str = args.mfa
     vpn_url: str = args.vpn_url
     headless: bool = args.show_head

+ 34 - 26
ocma/connect.py

@@ -3,20 +3,21 @@
 import logging
 import time
 from dataclasses import dataclass
-from typing import Optional
 from urllib.parse import urlparse
 
 from otppy import OTP
 from selenium import webdriver
-from selenium.common.exceptions import ElementClickInterceptedException
-from selenium.common.exceptions import ElementNotInteractableException
-from selenium.common.exceptions import NoSuchElementException
-from selenium.common.exceptions import StaleElementReferenceException
-from selenium.common.exceptions import TimeoutException
+from selenium.common.exceptions import (
+    ElementClickInterceptedException,
+    ElementNotInteractableException,
+    NoSuchElementException,
+    StaleElementReferenceException,
+    TimeoutException,
+)
 from selenium.webdriver.common.by import By
 from selenium.webdriver.firefox.options import Options as FirefoxOptions
-from selenium.webdriver.support import expected_conditions as EC
-from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as ec
+from selenium.webdriver.support.wait import WebDriverWait
 
 USERNAME_INPUT_NAME = "loginfmt"
 PASSWORD_INPUT_NAME = "passwd"
@@ -42,10 +43,10 @@ class VPNCookie:
     cookie: str
 
 
-def login(
+def login(  # noqa: PLR0913
     username: str,
     password: str,
-    mfa_secret: Optional[str] = None,
+    mfa_secret: str | None = None,
     vpn_site: str = "https://vpn.fhnw.ch",
     headless: bool = True,
     log_messages: bool = False,
@@ -59,9 +60,9 @@ def login(
         Microsoft account username.
     password : str
         Microsoft account password.
-    mfa_secret : Optional[str], optional
+    mfa_secret : str | None, optional
         Multifactor secret, by default None
-    vpn_site : _type_, optional
+    vpn_site : str, optional
         VPN site to log into, by default "https://vpn.fhnw.ch"
     headless : bool, optional
         If the browser should be run in headless mode, by default True
@@ -80,7 +81,7 @@ def login(
     """
     if log_messages:
         formatter = logging.Formatter(
-            "%(asctime)s: %(levelname)s - %(name)s - %(message)s"
+            "%(asctime)s: %(levelname)s - %(name)s - %(message)s",
         )
         fh_s = logging.StreamHandler()
         fh_s.setLevel(logging.INFO)
@@ -92,7 +93,7 @@ def login(
     options = FirefoxOptions()
     if headless:
         LOGGER.info("Running in headless mode")
-        options.add_argument("--headless")
+        options.add_argument("--headless")  # type: ignore
 
     driver = webdriver.Firefox(options=options)
     driver.get(vpn_site)
@@ -104,7 +105,7 @@ def login(
 
     if retries < 0:
         raise ValueError(
-            f"We never reached the MS login page! Currently on {driver.current_url}"
+            f"We never reached the MS login page! Currently on {driver.current_url}",
         )
 
     _fill_login(driver, username, password)
@@ -179,7 +180,7 @@ def on_login_form(driver: webdriver.Firefox) -> bool:
     return "saml2" in driver.current_url  # or driver.current_url.endswith("login")
 
 
-def _fill_mfa(driver: webdriver.Firefox, mfa_secret: Optional[str]) -> None:
+def _fill_mfa(driver: webdriver.Firefox, mfa_secret: str | None) -> None:
     LOGGER.info("Checking if we can fill in MFA information")
 
     _check_on_ms_login_page(driver)
@@ -197,7 +198,9 @@ def _fill_mfa(driver: webdriver.Firefox, mfa_secret: Optional[str]) -> None:
 
     for i in range(MFA_MAX_RETRY_COUNT):
         LOGGER.info(
-            "Filling in MFA information (Try %s of %s)", i + 1, MFA_MAX_RETRY_COUNT
+            "Filling in MFA information (Try %s of %s)",
+            i + 1,
+            MFA_MAX_RETRY_COUNT,
         )
         if not _find_element(driver, By.NAME, MFA_INPUT_NAME):
             time.sleep(ELEMENT_CHECK_DELAY)
@@ -266,7 +269,7 @@ def _confirm_stay_signed_in(driver: webdriver.Firefox) -> bool:
 
     if not driver.current_url.endswith("/common/SAS/ProcessAuth"):
         LOGGER.info(
-            "We have reached a dead end. We have completed the login, but not on the 'stay signed in' page"
+            "We have reached a dead end. We have completed the login, but not on the 'stay signed in' page",
         )
         return False
 
@@ -278,24 +281,29 @@ def _confirm_stay_signed_in(driver: webdriver.Firefox) -> bool:
 def _find_element(driver: webdriver.Firefox, by: str, item: str, wait: int = 8) -> bool:
     try:
         w = WebDriverWait(driver, wait)
-        w.until(EC.presence_of_element_located((by, item)))
-        return True
+        w.until(ec.presence_of_element_located((by, item)))
 
     except TimeoutException:
         return False
 
+    return True
+
 
 def _element_interactable(
-    driver: webdriver.Firefox, by: str, item: str, wait: int = 8
+    driver: webdriver.Firefox,
+    by: str,
+    item: str,
+    wait: int = 8,
 ) -> bool:
     try:
         w = WebDriverWait(driver, wait)
-        w.until(EC.element_to_be_clickable((by, item)))
-        return True
+        w.until(ec.element_to_be_clickable((by, item)))
 
     except ElementNotInteractableException:
         return False
 
+    return True
+
 
 def _is_on_install_page(driver: webdriver.Firefox) -> None:
     if not _find_element(driver, By.ID, VPN_INSTALL_PAGE_EXCLUSIVE_ELEMENT_ID):
@@ -308,7 +316,7 @@ def _get_webvpn_cookie(driver: webdriver.Firefox) -> VPNCookie:
 
     if webvpn_cookie is None:
         raise ValueError(
-            "Failed to find the webvpn cookie. Maybe the authentication has failed?"
+            "Failed to find the webvpn cookie. Maybe the authentication has failed?",
         )
 
     webvpn_domain = webvpn_cookie["domain"]
@@ -344,12 +352,12 @@ def click_continue(driver: webdriver.Firefox, btn_id: str = CONTINUE_BUTTON_ID)
     for _ in range(16):
         try:
             driver.find_element(By.ID, btn_id).click()
-            return True
-
         except (ElementClickInterceptedException, StaleElementReferenceException):
             pass
         except NoSuchElementException:
             pass
+        else:
+            return True
 
         time.sleep(ELEMENT_CHECK_DELAY)
     return False

+ 81 - 5
pyproject.toml

@@ -4,10 +4,7 @@ version = "0.3.0"
 description = ""
 authors = ["Sean Blackburn <birdicode@gmail.com>"]
 license = "MIT"
-packages = [
-  {include = "ocma"},
-  {include = "ocma/py.typed"},
-]
+packages = [{ include = "ocma" }, { include = "ocma/py.typed" }]
 
 [tool.poetry.dependencies]
 python = "^3.9"
@@ -30,4 +27,83 @@ build-backend = "poetry.core.masonry.api"
 ocma = 'ocma.cli:run'
 
 [tool.isort]
-multi_line_output=9
+profile = "black"
+
+
+[tool.ruff]
+target-version = "py311"
+extend-include = ["*.ipynb"]
+show-fixes = true
+
+[tool.ruff.lint]
+select = [
+  "E",   # pycodestyle
+  "F",   # Pyflakes
+  "UP",  # pyupgrade
+  "B",   # flake8-bugbear
+  "SIM", # flake8-simplify
+  "I",   # isort
+  "C90", # MCcabe
+  "N",   # pep8 naming
+  "D",   # pydocstyle
+  "UP",  # pyupgrade
+  "ICN", # import-conventions
+  "G",   # logging-format
+  "PIE", # PIE
+  "RET", # return values
+  "TCH", # type-checking
+  "PTH", # Use pathlib
+  "PD",  # pandas-vet
+  "PL",  # Pylint
+  "TRY", # tryceratops
+  "NPY", # NumPy-specific rules
+  "RUF", # Ruff-specific rules
+  # "FURB", # Refurb (Currently preview)
+  "PERF", # Perflint
+  "FA",   # Future annotations
+  "ISC",  # Implicit string concat
+  "INP",  # No __init__.py file
+  "C4",   # Comprehensions
+  "COM",  # Commas
+  "A",    # Builtin shadowing
+  "ANN",  # Annotations
+  "W",    # warning
+]
+ignore = ["E501", "ANN101", "ANN102", "TRY003"]
+
+[tool.ruff.lint.pep8-naming]
+ignore-names = []
+
+[tool.ruff.lint.pydocstyle]
+convention = "numpy"
+
+[tool.ruff.lint.per-file-ignores]
+"*.ipynb" = [
+  "D100", # No need to write a docstring at the top of the file
+  "B018", # It's okay to have a variable at the end of a cell to print its value
+]
+"*_test.py" = ["PLR2004"] # Test files may have constants without names
+
+[tool.ruff.lint.flake8-type-checking]
+runtime-evaluated-base-classes = []
+
+[tool.mypy]
+python_version = "3.11"
+warn_return_any = true
+warn_unused_configs = true
+strict = true
+pretty = true
+# disallow_untyped_decorators = false
+
+plugins = []
+
+[[tool.mypy.overrides]]
+ignore_missing_imports = true
+module = ["otppy.*"]
+
+# https://jsh9.github.io/pydoclint/config_options.html
+[tool.pydoclint]
+style = 'numpy'
+exclude = '\.git|data'
+allow-init-docstring = true
+skip-checking-short-docstrings = false