firebird-driver 2.0.0__tar.gz → 2.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firebird-driver
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Firebird driver for Python
5
5
  Project-URL: Home, https://github.com/FirebirdSQL/python3-driver
6
6
  Project-URL: Documentation, https://firebird-driver.rtfd.io
@@ -103,7 +103,7 @@ This project is using [pytest](https://docs.pytest.org/en/stable/) for testing,
103
103
  tests add several options via `tests/conftest.py`.
104
104
 
105
105
  By default, tests are configured to use local Firebird installation via network access.
106
- To use local instllation in `mebedded` mode, comment out the section:
106
+ To use local installation in `embedded` mode, comment out the section:
107
107
  ```
108
108
  [tool.hatch.envs.hatch-test]
109
109
  extra-args = ["--host=localhost"]
@@ -53,7 +53,7 @@ This project is using [pytest](https://docs.pytest.org/en/stable/) for testing,
53
53
  tests add several options via `tests/conftest.py`.
54
54
 
55
55
  By default, tests are configured to use local Firebird installation via network access.
56
- To use local instllation in `mebedded` mode, comment out the section:
56
+ To use local installation in `embedded` mode, comment out the section:
57
57
  ```
58
58
  [tool.hatch.envs.hatch-test]
59
59
  extra-args = ["--host=localhost"]
@@ -57,6 +57,9 @@ dependencies = [
57
57
 
58
58
  [tool.hatch.envs.hatch-test]
59
59
  extra-args = ["--host=localhost"]
60
+ extra-dependencies = [
61
+ "packaging>=25.0",
62
+ ]
60
63
 
61
64
  [[tool.hatch.envs.hatch-test.matrix]]
62
65
  python = ["3.11", "3.12", "3.13"]
@@ -119,7 +122,7 @@ ban-relative-imports = "all"
119
122
 
120
123
  [tool.ruff.lint.per-file-ignores]
121
124
  # Tests can use magic values, assertions, and relative imports
122
- "tests/**/*" = ["PLR2004", "S101", "TID252"]
125
+ "test_*" = ["PLR2004", "S101", "TID252"]
123
126
  "fbapi.py" = ["N801", "E501"]
124
127
  "interfaces.py" = ["ARG001", "ARG002", "N801", "N803", "E501", "FBT001"]
125
128
  "hooks.py" = ["F401"]
@@ -133,4 +133,4 @@ from .types import (
133
133
  )
134
134
 
135
135
  #: Current driver version, SEMVER string.
136
- __VERSION__ = '2.0.0'
136
+ __VERSION__ = '2.0.2'
@@ -53,6 +53,7 @@ import struct
53
53
  import sys
54
54
  import threading
55
55
  import weakref
56
+ from urllib.parse import urlparse
56
57
  from abc import ABC, abstractmethod
57
58
  from collections.abc import Callable, Mapping, Sequence
58
59
  from ctypes import addressof, byref, create_string_buffer, memmove, memset, pointer, string_at
@@ -1795,7 +1796,7 @@ class Connection:
1795
1796
  self.__FIREBIRD_LIB__ = None
1796
1797
  def __del__(self):
1797
1798
  if not self.is_closed():
1798
- warn(f"Connection '{self.logging_id}' disposed without prior close()", ResourceWarning)
1799
+ warn("Connection disposed without prior close()", ResourceWarning)
1799
1800
  self._close()
1800
1801
  self._close_internals()
1801
1802
  self._att.detach()
@@ -2179,6 +2180,75 @@ def _connect_helper(dsn: str, host: str, port: str, database: str, protocol: Net
2179
2180
  dsn += database
2180
2181
  return dsn
2181
2182
 
2183
+ def _is_dsn(value: str) -> bool:
2184
+ """
2185
+ Checks if the given string matches known patterns for Firebird DSNs.
2186
+
2187
+ This function analyzes the string for structures that are typical for
2188
+ Firebird connection strings, based on how the firebird-driver might
2189
+ construct them (per _connect_helper) or how the Firebird client
2190
+ library generally interprets them.
2191
+
2192
+ Args:
2193
+ value: The string to check.
2194
+
2195
+ Returns:
2196
+ True if the string matches a DSN pattern, False otherwise.
2197
+ """
2198
+ if not isinstance(value, str) or not value.strip():
2199
+ # Empty or whitespace-only strings are not DSNs
2200
+ return False
2201
+
2202
+ # 1. Protocol-based DSNs (e.g., inet://localhost/employee)
2203
+ # These are directly produced by _connect_helper if a protocol is specified.
2204
+ try:
2205
+ parsed = urlparse(value)
2206
+ if parsed.scheme in [p.name.lower() for p in NetProtocol]:
2207
+ # A scheme-based DSN must have a network location (host/port) or a path (database part).
2208
+ # parsed.path.lstrip('/') handles cases like "xnet:///path/to/db" where netloc is empty.
2209
+ if parsed.netloc or (parsed.path and parsed.path.lstrip('/')):
2210
+ return True
2211
+ except ValueError:
2212
+ # urlparse can raise ValueError for some malformed inputs (e.g. "::1")
2213
+ # These are unlikely to be scheme-based DSNs in the firebird context.
2214
+ pass
2215
+
2216
+ # 2. Windows Named Pipes (e.g., \\server\pipe_name or \\server@port\pipe_name)
2217
+ # These are indicated by starting with \\.
2218
+ if value.startswith("\\\\"):
2219
+ # Basic check: must have some content after \\, and not be just \\ or \\\
2220
+ if len(value) > 2 and value[2] not in ['\\', '/']:
2221
+ return True
2222
+
2223
+ # 3. Classic host:database or host/port:database syntax
2224
+ # (e.g., localhost:employee, server/3050:/data/db.fdb)
2225
+ # This pattern should not be confused with "C:\path" or "http://..."
2226
+ colon_idx = value.find(':')
2227
+ if colon_idx > 0 and "://" not in value[:colon_idx]: # Colon exists, not at start, and not part of a scheme
2228
+ host_spec = value[:colon_idx]
2229
+ # db_spec = value[colon_idx+1:] # Not strictly needed for this check
2230
+
2231
+ # Avoid misinterpreting "C:\path" as "host C, db \path".
2232
+ # If host_spec is a single letter (like a drive) AND the char after colon is a path separator,
2233
+ # it's more likely an absolute path. os.path.isabs handles these better.
2234
+ is_windows_drive_abs_path_candidate = (
2235
+ len(host_spec) == 1 and host_spec[0].isalpha() and
2236
+ len(value) > colon_idx + 1 and value[colon_idx+1] in ('/', '\\')
2237
+ )
2238
+
2239
+ if not is_windows_drive_abs_path_candidate:
2240
+ # host_spec should not contain backslashes if it's a hostname.
2241
+ # It can contain a forward slash for host/port.
2242
+ if '\\' not in host_spec:
2243
+ if '/' in host_spec: # Potential host/port:db
2244
+ # e.g., "server/3050:dbname"
2245
+ parts = host_spec.split('/', 1) # Split only on the first /
2246
+ if len(parts) == 2 and parts[0] and parts[1].isdigit(): # host part and port part (digits)
2247
+ return True
2248
+ elif host_spec: # Plain host:db, ensure host_spec is not empty
2249
+ # e.g., "localhost:dbname", "localhost:/path/to/db", "localhost:C:relative_path_on_C"
2250
+ return True
2251
+ return False
2182
2252
  def __make_connection(dsn: str, utf8filename: bool, dpb: bytes, sql_dialect: int, charset: str, # noqa: FBT001
2183
2253
  crypt_callback: iCryptKeyCallbackImpl, *, create: bool) -> Connection:
2184
2254
  with a.get_api().master.get_dispatcher() as provider:
@@ -2244,13 +2314,14 @@ def connect(database: str | Path, *, user: str | None=None, password: str | None
2244
2314
  if isinstance(database, Path):
2245
2315
  database = str(database)
2246
2316
  db_config: DatabaseConfig = driver_config.get_database(database)
2317
+ dsn: str | None = None
2247
2318
  if db_config is None:
2248
2319
  db_config = driver_config.db_defaults
2249
- # we'll assume that 'database' is 'dsn'
2250
- dsn = database
2251
- database = None
2252
2320
  srv_config = driver_config.server_defaults
2253
- srv_config.host.clear()
2321
+ if _is_dsn(database):
2322
+ dsn = database
2323
+ database = None
2324
+ srv_config.host.clear()
2254
2325
  else:
2255
2326
  database = db_config.database.value
2256
2327
  dsn = db_config.dsn.value
@@ -2328,11 +2399,11 @@ def create_database(database: str | Path, *, user: str | None=None, password: st
2328
2399
  db_config: DatabaseConfig = driver_config.get_database(database)
2329
2400
  if db_config is None:
2330
2401
  db_config = driver_config.db_defaults
2331
- # we'll assume that 'database' is 'dsn'
2332
- dsn = database
2333
- database = None
2334
2402
  srv_config = driver_config.server_defaults
2335
- srv_config.host.clear()
2403
+ if _is_dsn(database):
2404
+ dsn = database
2405
+ database = None
2406
+ srv_config.host.clear()
2336
2407
  else:
2337
2408
  database = db_config.database.value
2338
2409
  dsn = db_config.dsn.value
@@ -2537,7 +2608,7 @@ class TransactionManager:
2537
2608
  self.close()
2538
2609
  def __del__(self):
2539
2610
  if self._tra is not None:
2540
- warn(f"Transaction '{self.logging_id}' disposed while active", ResourceWarning)
2611
+ warn("Transaction disposed while active", ResourceWarning)
2541
2612
  self._finish()
2542
2613
  def __dead_con(self, obj: Connection) -> None: # noqa: ARG002
2543
2614
  self._connection = None
@@ -2947,12 +3018,8 @@ class Statement:
2947
3018
  self.free()
2948
3019
  def __del__(self):
2949
3020
  if self._in_meta or self._out_meta or self._istmt:
2950
- warn(f"Statement '{self.logging_id}' disposed without prior free()", ResourceWarning)
3021
+ warn("Statement disposed without prior free()", ResourceWarning)
2951
3022
  self.free()
2952
- def __str__(self):
2953
- return f'{self.logging_id}[{self.sql}]'
2954
- def __repr__(self):
2955
- return str(self)
2956
3023
  def __dead_con(self, obj: Connection) -> None: # noqa: ARG002
2957
3024
  self._connection = None
2958
3025
  def __get_plan(self, *, detailed: bool) -> str:
@@ -3085,10 +3152,8 @@ class BlobReader(io.IOBase):
3085
3152
  self.close()
3086
3153
  def __del__(self):
3087
3154
  if self._blob is not None:
3088
- warn(f"BlobReader '{self.logging_id}' disposed without prior close()", ResourceWarning)
3155
+ warn("BlobReader disposed without prior close()", ResourceWarning)
3089
3156
  self.close()
3090
- def __repr__(self):
3091
- return f'{self.logging_id}[size={self.length}]'
3092
3157
  def flush(self) -> None:
3093
3158
  """Does nothing.
3094
3159
  """
@@ -3292,7 +3357,7 @@ class Cursor:
3292
3357
  self.close()
3293
3358
  def __del__(self):
3294
3359
  if self._result is not None or self._stmt is not None or self.__blob_readers:
3295
- warn(f"Cursor '{self.logging_id}' disposed without prior close()", ResourceWarning)
3360
+ warn("Cursor disposed without prior close()", ResourceWarning)
3296
3361
  self.close()
3297
3362
  def __next__(self):
3298
3363
  if (row := self.fetchone()) is not None:
@@ -5547,7 +5612,7 @@ class Server:
5547
5612
  self.close()
5548
5613
  def __del__(self):
5549
5614
  if self._svc is not None:
5550
- warn(f"Server '{self.logging_id}' disposed without prior close()", ResourceWarning)
5615
+ warn("Server disposed without prior close()", ResourceWarning)
5551
5616
  self.close()
5552
5617
  def __next__(self):
5553
5618
  if (line := self.readline()) is not None:
File without changes