Skip to content

Commit e5bb26c

Browse files
committed
Add comprehensive ruff linting configuration
Build on the ruff formatting added in #157 by adding full linting with select = ["ALL"] per our strict Python guide. This enforces all ruff rules with explicit, documented exclusions for rules that are inappropriate for the existing codebase. Changes: - Add [tool.ruff.lint] config to pyproject.toml with select = ["ALL"] - Add per-file-ignores for test, demo, and docs files - Add isort config with known-first-party = ["mixpanel"] - Add pydocstyle convention = "google" - Add ruff check step to CI workflow - Add .pre-commit-config.yaml with ruff format + lint hooks - Add pre-commit to dev dependencies - Modernize type annotations (Dict -> dict, List -> list) - Add from __future__ import annotations to flags modules - Convert logging f-strings to lazy %s formatting - Add module-level loggers replacing root logger calls - Fix docstring formatting (blank lines, punctuation) - Fix true/false comparisons (== True -> is True) - Fix raise-without-from in exception handler - Extract f-strings from exception constructors - Various small fixes (SIM102, SIM118, UP030, C408, etc.)
1 parent 7aa7123 commit e5bb26c

16 files changed

Lines changed: 383 additions & 297 deletions

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ jobs:
1414
uses: astral-sh/setup-uv@v5
1515
- name: Check formatting
1616
run: uvx ruff format --check .
17+
- name: Check linting
18+
run: uvx ruff check .
1719

1820
test:
1921
runs-on: ubuntu-24.04

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.9.10
4+
hooks:
5+
- id: ruff-format
6+
- id: ruff
7+
args: [--fix]

demo/local_flags.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import os
21
import asyncio
3-
import mixpanel
42
import logging
53

4+
import mixpanel
5+
66
logging.basicConfig(level=logging.INFO)
77

88
# Configure your project token, the feature flag to test, and user context to evaluate.

demo/remote_flags.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import asyncio
2-
import mixpanel
32
import logging
43

4+
import mixpanel
5+
56
logging.basicConfig(level=logging.INFO)
67

78
# Configure your project token, the feature flag to test, and user context to evaluate.

demo/subprocess_consumer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import multiprocessing
22
import random
33

4-
from mixpanel import Mixpanel, BufferedConsumer
4+
from mixpanel import BufferedConsumer, Mixpanel
55

66
"""
77
As your application scales, it's likely you'll want to
@@ -23,7 +23,7 @@
2323
"""
2424

2525

26-
class QueueWriteConsumer(object):
26+
class QueueWriteConsumer:
2727
def __init__(self, queue):
2828
self.queue = queue
2929

docs/conf.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
# -*- coding: utf-8 -*-
21
import sys
3-
import os
2+
from pathlib import Path
43

54
# If extensions (or modules to document with autodoc) are in another directory,
65
# add these directories to sys.path here. If the directory is relative to the
7-
# documentation root, use os.path.abspath to make it absolute, like shown here.
8-
sys.path.insert(0, os.path.abspath(".."))
6+
# documentation root, use Path.resolve() to make it absolute, like shown here.
7+
sys.path.insert(0, str(Path("..").resolve()))
98

109
extensions = [
1110
"sphinx.ext.autodoc",

mixpanel/__init__.py

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# -*- coding: utf-8 -*-
21
"""This is the official Mixpanel client library for Python.
32
43
Mixpanel client libraries allow for tracking events and setting properties on
@@ -20,12 +19,11 @@
2019
import logging
2120
import time
2221
import uuid
22+
from typing import Optional
2323

2424
import requests
25-
from requests.auth import HTTPBasicAuth
2625
import urllib3
27-
28-
from typing import Optional
26+
from requests.auth import HTTPBasicAuth
2927

3028
from .flags.local_feature_flags import LocalFeatureFlagsProvider
3129
from .flags.remote_feature_flags import RemoteFeatureFlagsProvider
@@ -98,7 +96,7 @@ def _make_insert_id(self):
9896

9997
@property
10098
def local_flags(self) -> LocalFeatureFlagsProvider:
101-
"""Get the local flags provider if configured for it"""
99+
"""Get the local flags provider if configured for it."""
102100
if self._local_flags_provider is None:
103101
raise MixpanelException(
104102
"No local flags provider initialized. Pass local_flags_config to constructor."
@@ -107,7 +105,7 @@ def local_flags(self) -> LocalFeatureFlagsProvider:
107105

108106
@property
109107
def remote_flags(self) -> RemoteFeatureFlagsProvider:
110-
"""Get the remote flags provider if configured for it"""
108+
"""Get the remote flags provider if configured for it."""
111109
if self._remote_flags_provider is None:
112110
raise MixpanelException(
113111
"No remote_flags_config was passed to the consttructor"
@@ -182,7 +180,6 @@ def import_data(
182180
for `more details
183181
<https://developer.mixpanel.com/reference/events#import-events>`__.
184182
"""
185-
186183
if api_secret is None:
187184
logger.warning(
188185
"api_key will soon be removed from mixpanel-python; please use api_secret instead."
@@ -242,8 +239,7 @@ def alias(self, alias_id, original, meta=None):
242239
sync_consumer.send("events", json_dumps(event, cls=self._serializer))
243240

244241
def merge(self, api_key, distinct_id1, distinct_id2, meta=None, api_secret=None):
245-
"""
246-
Merges the two given distinct_ids.
242+
"""Merges the two given distinct_ids.
247243
248244
:param str api_key: (DEPRECATED) Your Mixpanel project's API key.
249245
:param str distinct_id1: The first distinct_id to merge.
@@ -587,7 +583,7 @@ def group_delete(self, group_key, group_id, meta=None):
587583
)
588584

589585
def group_update(self, message, meta=None):
590-
"""Send a generic group profile update
586+
"""Send a generic group profile update.
591587
592588
:param dict message: the message to send
593589
@@ -626,20 +622,18 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
626622
await self._remote_flags_provider.__aexit__(exc_type, exc_val, exc_tb)
627623

628624

629-
class MixpanelException(Exception):
625+
class MixpanelException(Exception): # noqa: N818
630626
"""Raised by consumers when unable to send messages.
631627
632628
This could be caused by a network outage or interruption, or by an invalid
633629
endpoint passed to :meth:`.Consumer.send`.
634630
"""
635631

636-
pass
637632

633+
class Consumer:
634+
"""A consumer that sends an HTTP request directly to the Mixpanel service.
638635
639-
class Consumer(object):
640-
"""
641-
A consumer that sends an HTTP request directly to the Mixpanel service, one
642-
per call to :meth:`~.send`.
636+
One per call to :meth:`~.send`.
643637
644638
:param str events_url: override the default events API endpoint
645639
:param str people_url: override the default people API endpoint
@@ -674,10 +668,10 @@ def __init__(
674668
):
675669
# TODO: With next major version, make the above args kwarg-only, and reorder them.
676670
self._endpoints = {
677-
"events": events_url or "https://{}/track".format(api_host),
678-
"people": people_url or "https://{}/engage".format(api_host),
679-
"groups": groups_url or "https://{}/groups".format(api_host),
680-
"imports": import_url or "https://{}/import".format(api_host),
671+
"events": events_url or f"https://{api_host}/track",
672+
"people": people_url or f"https://{api_host}/engage",
673+
"groups": groups_url or f"https://{api_host}/groups",
674+
"imports": import_url or f"https://{api_host}/import",
681675
}
682676

683677
self._verify_cert = verify_cert
@@ -717,11 +711,8 @@ def send(self, endpoint, json_message, api_key=None, api_secret=None):
717711
The *api_secret* parameter.
718712
"""
719713
if endpoint not in self._endpoints:
720-
raise MixpanelException(
721-
'No such endpoint "{0}". Valid endpoints are one of {1}'.format(
722-
endpoint, self._endpoints.keys()
723-
)
724-
)
714+
msg = f'No such endpoint "{endpoint}". Valid endpoints are one of {self._endpoints.keys()}'
715+
raise MixpanelException(msg)
725716

726717
self._write_request(
727718
self._endpoints[endpoint], json_message, api_key, api_secret
@@ -759,22 +750,19 @@ def _write_request(self, request_url, json_message, api_key=None, api_secret=Non
759750
try:
760751
response_dict = response.json()
761752
except ValueError:
762-
raise MixpanelException(
763-
"Cannot interpret Mixpanel server response: {0}".format(response.text)
764-
)
753+
msg = f"Cannot interpret Mixpanel server response: {response.text}"
754+
raise MixpanelException(msg) from None
765755

766756
if response_dict["status"] != 1:
767-
raise MixpanelException(
768-
"Mixpanel error: {0}".format(response_dict["error"])
769-
)
757+
raise MixpanelException("Mixpanel error: {}".format(response_dict["error"]))
770758

771759
return True # <- TODO: remove return val with major release.
772760

773761

774-
class BufferedConsumer(object):
775-
"""
776-
A consumer that maintains per-endpoint buffers of messages and then sends
777-
them in batches. This can save bandwidth and reduce the total amount of
762+
class BufferedConsumer:
763+
"""A consumer that maintains per-endpoint buffers of messages and then sends them in batches.
764+
765+
This can save bandwidth and reduce the total amount of
778766
time required to post your events to Mixpanel.
779767
780768
:param int max_size: number of :meth:`~.send` calls for a given endpoint to
@@ -857,18 +845,15 @@ def send(self, endpoint, json_message, api_key=None, api_secret=None):
857845
The *api_key* parameter.
858846
"""
859847
if endpoint not in self._buffers:
860-
raise MixpanelException(
861-
'No such endpoint "{0}". Valid endpoints are one of {1}'.format(
862-
endpoint, self._buffers.keys()
863-
)
864-
)
848+
msg = f'No such endpoint "{endpoint}". Valid endpoints are one of {self._buffers.keys()}'
849+
raise MixpanelException(msg)
865850

866851
if not isinstance(api_key, tuple):
867852
api_key = (api_key, api_secret)
868853

869854
buf = self._buffers[endpoint]
870855
buf.append(json_message)
871-
# Fixme: Don't stick these in the instance.
856+
# TODO: Don't stick these in the instance.
872857
self._api_key = api_key
873858
self._api_secret = api_secret
874859
if len(buf) >= self._max_size:
@@ -880,15 +865,15 @@ def flush(self):
880865
:raises MixpanelException: if the server is unreachable or any buffered
881866
message cannot be processed
882867
"""
883-
for endpoint in self._buffers.keys():
868+
for endpoint in self._buffers:
884869
self._flush_endpoint(endpoint)
885870

886871
def _flush_endpoint(self, endpoint):
887872
buf = self._buffers[endpoint]
888873

889874
while buf:
890875
batch = buf[: self._max_size]
891-
batch_json = "[{0}]".format(",".join(batch))
876+
batch_json = "[{}]".format(",".join(batch))
892877
try:
893878
self._consumer.send(endpoint, batch_json, api_key=self._api_key)
894879
except MixpanelException as orig_e:

0 commit comments

Comments
 (0)