ADR-057: Rename a symbol¶
Status¶
Accepted.
Decision¶
A library that keeps its package can move a name out of one of its submodules, and a pack states
that with a rename_symbol change. openai.error.RateLimitError becomes openai.RateLimitError,
and the three classes of openai.error that a local server shows to be raised for the same
responses on both sides are the ones the openai pack (ADR-053) renames. The kind is the eighth,
and the registry of ADR-006 lists it.
D1. A class is renamed only where the same responses raise it¶
symbols maps a legacy dotted path in a submodule to the name it takes in the package above it, and
now_caught_by lists what the new class derives from and the old one did not. A pack lists a class
only when a handler for the old one runs for exactly the responses a handler for the new one does:
the old class is raised for a set of responses, and the new class, with its bases, is caught for the
same set. That is measured, not read off the guide, against a local server that answers 0.28.1 and
the new releases alike (tests/packs/openai_legacy_check.py for the old side, held in
tests/packs/openai_legacy_surface.json, and tests/packs/test_openai_facts.py for the new):
| Response | 0.27.0 and 0.28.1 | 1.109.1 and later |
|---|---|---|
| status 400, 404, 415 | InvalidRequestError |
BadRequestError, NotFoundError, APIStatusError |
| status 401 | AuthenticationError |
AuthenticationError |
| status 403 | PermissionError |
PermissionDeniedError |
| status 429 | RateLimitError |
RateLimitError |
| status 500 | APIError |
InternalServerError |
| status 503 | ServiceUnavailableError |
InternalServerError |
| an empty 200 body | APIError |
json.JSONDecodeError |
| no API key | AuthenticationError |
OpenAIError (up to 2.0), TypeError (from 2.54) |
| a read timeout | Timeout |
APITimeoutError |
| a refused connection | APIConnectionError |
APIConnectionError |
RateLimitError, PermissionError (as PermissionDeniedError) and Timeout (as APITimeoutError)
pass. AuthenticationError and OpenAIError do not: a missing key was an AuthenticationError, so
an OpenAIError, and is a bare OpenAIError or a TypeError now, which no AuthenticationError
handler catches and, for the TypeError, no OpenAIError handler either, and an empty 200 body was
an APIError, so an OpenAIError, and is a json.JSONDecodeError. InvalidRequestError is three
classes, ServiceUnavailableError is a part of one, and APIError and APIConnectionError are
bases that catch more. TryAgain was raised only for a call with a timeout argument, and
InvalidAPIType and SignatureVerificationError have no counterpart. Each of them stays in a
flag_only change whose message says what it became, and tests/packs/test_openai_facts.py asserts
for each that no class of the new release is raised for exactly its responses, so the refusal stays
true when a release changes.
Rejected: renaming OpenAIError and AuthenticationError and listing the missing key as a
limitation. They are the classes most handlers name (eighteen of fifty-eight rows), and the pack
refuses the same thing elsewhere: a handler that stops running for a failure it handled is what
keeps Image.create(response_format=...) out of rewrite_call. Rejected: mapping a class to a
tuple of the new ones, which would add classes a handler never caught.
D2. What is written¶
A reference loses the submodule, and a class that takes another name takes it:
openai.error.RateLimitErrorisopenai.RateLimitError, through whatever name the file bound the package to (oa.error.PermissionErrorisoa.PermissionDeniedError).import openai.errorbinds the package, so it becomesimport openai, andimport openai.error as faultsbecomesimport openai as faults: the alias is kept andfaults.RateLimitErrorneeds no other change. A line of several statements becomes one line each.from openai.error import RateLimitError as Limitedbecomesfrom openai import RateLimitError as Limited. Without an alias a class that takes another name is renamed where the file reads it (PermissionErrorisPermissionDeniedError), unless the file already binds that name.
The rule claims the finding of a reference and of the import of a submodule it renames. The import
statement is replaced through the import plan (ADR-025) and a reference through Rewrites.set, so
the two are one pass with the other rules'. A reference the scan found is a chain or a bound name; a
string annotation is a mention only, which is no usage.
D3. What is refused¶
No new bail code: each of these is a code the vocabulary has, with a wider row.
error_class_changed: a class that is called, raised or derived from, since a new status class is built frommessage, theresponseand thebodyand a timeout from therequest, where an old one took the message and the body, status and headers.error_class_changed: a handler ahead of a renamed class in the sametry(anexcept*too) that names a class innow_caught_by(openai.APIError, which 0.28.1 has as a sibling and the new releases as the base of every status class) or one the rule cannot tell: a name bound in the file, a call, an attribute ofself. A builtin, a name imported from another module and a class the same change renames pass: the last is a sibling of the new class, and a name another of your modules re-exports fromopenaiis not seen, as for every rule of a shared module.attribute_removed: anexceptclause that reads an attribute of its bound name, in the body or any function in it. The new classes havestatus_code,bodyandmessagewhere the old hadhttp_status,json_bodyanduser_message, and an attribute of one name may mean another. Passing the exception on (print(error),raise Wrapper(error) from error) reads nothing.from_import_unmigrated_symbol: afrom openai.error importthat names a class the change does not rename,from openai import error, and an import in a one-line suite.alias_collision: a class that takes another name where the file binds that name.
D4. What the scan reports¶
openai.error leaves the symbols a flag_only change refuses, and each class the change does not
rename is listed by name in a flag_only change whose message says what it became. A rename
under a refused prefix would never fire, since the scan grades a refused symbol first, so a pack
that has one is refused. The import of the submodule is an eligible row of the new change; a use of
the module that is no listed class (errors = openai.error, openai.error.helper) is an eligible
row nothing claims, which withholds the file as usage_unmapped, as an unlisted use did.
D5. What validation checks¶
The kind is for a package that keeps its name, so a pack with match.shared false is refused: a
package that moves is renamed by rename_import. Every symbol is a path of three names or more and
under match.symbols, none is named by two changes, renamed onto a keyword or onto one the pack
lists as legacy, and no two land on one name. now_caught_by is sorted and de-duplicated. Each
refusal has a negative pack.
D6. What was not measured, and is a limitation¶
- The text of an exception changes:
str(error)was the API's message, withRequest <id>:before it for a response that had one, and isError code: <status> - <body>now. A handler that parses it sees the message inside the body. - A response that is no API error (a proxy's HTML page, an object with no
errorkey) was anAPIErroron 0.28.1 whatever its status, and is the class of its status on the new releases, so a handler for a renamed class catches a response it did not. - A handler for
openai.APIError, which both sides define and the pack does not report, catches every status class on the new releases and caught the 5xx and malformed responses alone on 0.28.1. The rule reads the handlers of onetry; one in a caller, or reached by a name, is not seen. - A class given to a mock (
Mock(side_effect=openai.error.RateLimitError)) or kept in a name that is called later (E = openai.error.RateLimitError; raise E("x")) is built with no arguments or with the old ones, which the new class refuses with aTypeError: a test fails loudly where 0.28.1's passed.getattr(error, "http_status")andvars(error)on a bound name read an attribute the rule does not see. import openaibesideimport openai.erroris a name bound by two imports, which the scan reads asconditional_bindingand withholds, as it did before the rename.
Consequences¶
- A handler or a decorator argument for one of three classes no longer holds the repository back by
that line. A repository that names
OpenAIError,AuthenticationError,InvalidRequestErrororAPIErrorstill does, with a message that names what each became. - The kind is for one real use. A second pack with a package that moved its classes will say whether
now_caught_byshould be a table per class.