Whole file
joke2k/django-environ
The author described this change as “Fix `environ.Path.__eq__()` to compare paths correctly”. It counts as a record because the check below fails on the code as it stood at ea308232d and passes on 27a8a8735, with nothing else changed between the two runs.
Projectjoke2k/django-environ
Fix saved2022-06-14
Sharing licenceMIT · LICENSE.txt
Change size+643 −637
What the code was meant to do, written into the code itself as a save note
Fix `environ.Path.__eq__()` to compare paths correctly
The change
| 361 | 361 | def get_value(self, var, cast=None, default=NOTSET, parse_default=False): | |
| 362 | 362 | """Return value for given environment variable. | |
| 363 | 363 | ||
| 364 | - | :param var: Name of variable. | |
| 365 | - | :param cast: Type to cast return value as. | |
| 366 | - | :param default: If var not present in environ, return this instead. | |
| 367 | - | :param parse_default: force to parse default.. | |
| 368 | - | ||
| 369 | - | :returns: Value from environment or default (if set) | |
| 370 | - | """ | |
| 371 | - | ||
| 372 | - | logger.debug("get '{}' casted as '{}' with default '{}'".format( | |
| 373 | - | var, cast, default | |
| 374 | - | )) | |
| 375 | - | ||
| 376 | - | var_name = "{}{}".format(self.prefix, var) | |
| 377 | - | if var_name in self.scheme: | |
| 378 | - | var_info = self.scheme[var_name] | |
| 379 | - | ||
| 380 | - | try: | |
| 381 | - | has_default = len(var_info) == 2 | |
| 382 | - | except TypeError: | |
| 383 | - | has_default = False | |
| 384 | - | ||
| 385 | - | if has_default: | |
| 386 | - | if not cast: | |
| 387 | - | cast = var_info[0] | |
| 388 | - | ||
| 389 | - | if default is self.NOTSET: | |
| 390 | - | try: | |
| 391 | - | default = var_info[1] | |
| 392 | - | except IndexError: | |
| 393 | - | pass | |
| 394 | - | else: | |
| 395 | - | if not cast: | |
| 396 | - | cast = var_info | |
| 397 | - | ||
| 398 | - | try: | |
| 399 | - | value = self.ENVIRON[var_name] | |
| 400 | - | except KeyError as exc: | |
| 401 | - | if default is self.NOTSET: | |
| 402 | - | error_msg = "Set the {} environment variable".format(var) | |
| 403 | - | raise ImproperlyConfigured(error_msg) from exc | |
| 404 | - | ||
| 405 | - | value = default | |
| 406 | - | ||
| 407 | - | # Resolve any proxied values | |
| 408 | - | prefix = b'$' if isinstance(value, bytes) else '$' | |
| 409 | - | escape = rb'\$' if isinstance(value, bytes) else r'\$' | |
| 410 | - | if hasattr(value, 'startswith') and value.startswith(prefix): | |
| 411 | - | value = value.lstrip(prefix) | |
| 412 | - | value = self.get_value(value, cast=cast, default=default) | |
| 413 | - | ||
| 414 | - | if self.escape_proxy and hasattr(value, 'replace'): | |
| 415 | - | value = value.replace(escape, prefix) | |
| 416 | - | ||
| 417 | - | # Smart casting | |
| 418 | - | if self.smart_cast: | |
| 419 | - | if cast is None and default is not None and \ | |
| 420 | - | not isinstance(default, NoValue): | |
| 421 | - | cast = type(default) | |
| 422 | - | ||
| 423 | - | value = None if default is None and value == '' else value | |
| 424 | - | ||
| 425 | - | if value != default or (parse_default and value): | |
| 426 | - | value = self.parse_value(value, cast) | |
| 427 | - | ||
| 428 | - | return value | |
| 429 | - | ||
| 430 | - | # Class and static methods | |
| 431 | - | ||
| 432 | - | @classmethod | |
| 433 | - | def parse_value(cls, value, cast): | |
| 434 | - | """Parse and cast provided value | |
| 435 | - | ||
| 436 | - | :param value: Stringed value. | |
| 437 | - | :param cast: Type to cast return value as. | |
| 438 | - | ||
| 439 | - | :returns: Casted value | |
| 440 | - | """ | |
| 441 | - | if cast is None: | |
| 442 | - | return value | |
| 443 | - | elif cast is bool: | |
| 444 | - | try: | |
| 445 | - | value = int(value) != 0 | |
| 446 | - | except ValueError: | |
| 447 | - | value = value.lower() in cls.BOOLEAN_TRUE_STRINGS | |
| 448 | - | elif isinstance(cast, list): | |
| 449 | - | value = list(map(cast[0], [x for x in value.split(',') if x])) | |
| 450 | - | elif isinstance(cast, tuple): | |
| 451 | - | val = value.strip('(').strip(')').split(',') | |
| 452 | - | value = tuple(map(cast[0], [x for x in val if x])) | |
| 453 | - | elif isinstance(cast, dict): | |
| 454 | - | key_cast = cast.get('key', str) | |
| 455 | - | value_cast = cast.get('value', str) | |
| 456 | - | value_cast_by_key = cast.get('cast', dict()) | |
| 457 | - | value = dict(map( | |
| 458 | - | lambda kv: ( | |
| 459 | - | key_cast(kv[0]), | |
| 460 | - | cls.parse_value( | |
| 461 | - | kv[1], | |
| 462 | - | value_cast_by_key.get(kv[0], value_cast) | |
| 463 | - | ) | |
| 464 | - | ), | |
| 465 | - | [val.split('=') for val in value.split(';') if val] | |
| 466 | - | )) | |
| 467 | - | elif cast is dict: | |
| 468 | - | value = dict([val.split('=') for val in value.split(',') if val]) | |
| 469 | - | elif cast is list: | |
| 470 | - | value = [x for x in value.split(',') if x] | |
| 471 | - | elif cast is tuple: | |
| 472 | - | val = value.strip('(').strip(')').split(',') | |
| 473 | - | value = tuple([x for x in val if x]) | |
| 474 | - | elif cast is float: | |
| 475 | - | # clean string | |
| 476 | - | float_str = re.sub(r'[^\d,.-]', '', value) | |
| 477 | - | # split for avoid thousand separator and different | |
| 478 | - | # locale comma/dot symbol | |
| 479 | - | parts = re.split(r'[,.]', float_str) | |
| 480 | - | if len(parts) == 1: | |
| 481 | - | float_str = parts[0] | |
| 482 | - | else: | |
| 483 | - | float_str = "{}.{}".format(''.join(parts[0:-1]), parts[-1]) | |
| 484 | - | value = float(float_str) | |
| 485 | - | else: | |
| 486 | - | value = cast(value) | |
| 487 | - | return value | |
| 488 | - | ||
| 489 | - | @classmethod | |
| 490 | - | def db_url_config(cls, url, engine=None): | |
| 491 | - | """Parse an arbitrary database URL. | |
| 492 | - | ||
| 493 | - | Supports the following URL schemas: | |
| 494 | - | ||
| 495 | - | * PostgreSQL: ``postgres[ql]?://`` or ``p[g]?sql://`` | |
| 496 | - | * PostGIS: ``postgis://`` | |
| 497 | - | * MySQL: ``mysql://`` or ``mysql2://`` | |
| 498 | - | * MySQL (GIS): ``mysqlgis://`` | |
| 499 | - | * MySQL Connector Python from Oracle: ``mysql-connector://`` | |
| 500 | - | * SQLite: ``sqlite://`` | |
| 501 | - | * SQLite with SpatiaLite for GeoDjango: ``spatialite://`` | |
| 502 | - | * Oracle: ``oracle://`` | |
| 503 | - | * Microsoft SQL Server: ``mssql://`` | |
| 504 | - | * PyODBC: ``pyodbc://`` | |
| 505 | - | * Amazon Redshift: ``redshift://`` | |
| 506 | - | * LDAP: ``ldap://`` | |
| 507 | - | ||
| 508 | - | :param urllib.parse.ParseResult or str url: | |
| 509 | - | Database URL to parse. | |
| 510 | - | :param str or None engine: | |
| 511 | - | If None, the database engine is evaluates from the ``url``. | |
| 512 | - | :return: Parsed database URL. | |
| 513 | - | :rtype: dict | |
| 514 | - | """ | |
| 515 | - | if not isinstance(url, cls.URL_CLASS): | |
| 516 | - | if url == 'sqlite://:memory:': | |
| 517 | - | # this is a special case, because if we pass this URL into | |
| 518 | - | # urlparse, urlparse will choke trying to interpret "memory" | |
| 519 | - | # as a port number | |
| 520 | - | return { | |
| 521 | - | 'ENGINE': cls.DB_SCHEMES['sqlite'], | |
| 522 | - | 'NAME': ':memory:' | |
| 523 | - | } | |
| 524 | - | # note: no other settings are required for sqlite | |
| 525 | - | url = urlparse(url) | |
| 526 | - | ||
| 527 | - | config = {} | |
| 528 | - | ||
| 529 | - | # Remove query strings. | |
| 530 | - | path = url.path[1:] | |
| 531 | - | path = unquote_plus(path.split('?', 2)[0]) | |
| 532 | - | ||
| 533 | - | if url.scheme == 'sqlite': | |
| 534 | - | if path == '': | |
| 535 | - | # if we are using sqlite and we have no path, then assume we | |
| 536 | - | # want an in-memory database (this is the behaviour of | |
| 537 | - | # sqlalchemy) | |
| 538 | - | path = ':memory:' | |
| 539 | - | if url.netloc: | |
| 540 | - | warnings.warn('SQLite URL contains host component %r, ' | |
| 541 | - | 'it will be ignored' % url.netloc, stacklevel=3) | |
| 542 | - | if url.scheme == 'ldap': | |
| 543 | - | path = '{scheme}://{hostname}'.format( | |
| 544 | - | scheme=url.scheme, | |
| 545 | - | hostname=url.hostname, | |
| 546 | - | ) | |
| 547 | - | if url.port: | |
| 548 | - | path += ':{port}'.format(port=url.port) | |
| 549 | - | ||
| 550 | - | user_host = url.netloc.rsplit('@', 1) | |
| 551 | - | if url.scheme in cls.POSTGRES_FAMILY and ',' in user_host[-1]: | |
| 552 | - | # Parsing postgres cluster dsn | |
| 553 | - | hinfo = list( | |
| 554 | - | itertools.zip_longest( | |
| 555 | - | *( | |
| 556 | - | host.rsplit(':', 1) | |
| 557 | - | for host in user_host[-1].split(',') | |
| 558 | - | ) | |
| 559 | - | ) | |
| 560 | - | ) | |
| 561 | - | hostname = ','.join(hinfo[0]) | |
| 562 | - | port = ','.join(filter(None, hinfo[1])) if len(hinfo) == 2 else '' | |
| 563 | - | else: | |
| 564 | - | hostname = url.hostname | |
| 565 | - | port = url.port | |
| 566 | - | ||
| 567 | - | # Update with environment configuration. | |
| 568 | - | config.update({ | |
| 569 | - | 'NAME': path or '', | |
| 570 | - | 'USER': _cast_urlstr(url.username) or '', | |
| 571 | - | 'PASSWORD': _cast_urlstr(url.password) or '', | |
| 572 | - | 'HOST': hostname or '', | |
| 573 | - | 'PORT': _cast_int(port) or '', | |
| 574 | - | }) | |
| 575 | - | ||
| 576 | - | if ( | |
| 577 | - | url.scheme in cls.POSTGRES_FAMILY and path.startswith('/') | |
| 578 | - | or cls.CLOUDSQL in path and path.startswith('/') | |
| 579 | - | ): | |
| 580 | - | config['HOST'], config['NAME'] = path.rsplit('/', 1) | |
| 581 | - | ||
| 582 | - | if url.scheme == 'oracle' and path == '': | |
| 583 | - | config['NAME'] = config['HOST'] | |
| 584 | - | config['HOST'] = '' | |
| 585 | - | ||
| 586 | - | if url.scheme == 'oracle': | |
| 587 | - | # Django oracle/base.py strips port and fails on non-string value | |
| 588 | - | if not config['PORT']: | |
| 589 | - | del (config['PORT']) | |
| 590 | - | else: | |
| 591 | - | config['PORT'] = str(config['PORT']) | |
| 592 | - | ||
| 593 | - | if url.query: | |
| 594 | - | config_options = {} | |
| 595 | - | for k, v in parse_qs(url.query).items(): | |
| 596 | - | if k.upper() in cls._DB_BASE_OPTIONS: | |
| 597 | - | config.update({k.upper(): _cast(v[0])}) | |
| 598 | - | else: | |
| 599 | - | config_options.update({k: _cast_int(v[0])}) | |
| 600 | - | config['OPTIONS'] = config_options | |
| 601 | - | ||
| 602 | - | if engine: | |
| 603 | - | config['ENGINE'] = engine | |
| 604 | - | else: | |
| 605 | - | config['ENGINE'] = url.scheme | |
| 606 | - | ||
| 607 | - | if config['ENGINE'] in Env.DB_SCHEMES: | |
| 608 | - | config['ENGINE'] = Env.DB_SCHEMES[config['ENGINE']] | |
| 609 | - | ||
| 610 | - | if not config.get('ENGINE', False): | |
| 611 | - | warnings.warn("Engine not recognized from url: {}".format(config)) | |
| 612 | - | return {} | |
| 613 | - | ||
| 614 | - | return config | |
| 615 | - | ||
| 616 | - | @classmethod | |
| 617 | - | def cache_url_config(cls, url, backend=None): | |
| 618 | - | """Parse an arbitrary cache URL. | |
| 619 | - | ||
| 620 | - | :param urllib.parse.ParseResult or str url: | |
| 621 | - | Cache URL to parse. | |
| 622 | - | :param str or None backend: | |
| 623 | - | If None, the backend is evaluates from the ``url``. | |
| 624 | - | :return: Parsed cache URL. | |
| 625 | - | :rtype: dict | |
| 626 | - | """ | |
| 627 | - | if not isinstance(url, cls.URL_CLASS): | |
| 628 | - | if not url: | |
| 629 | - | return {} | |
| 630 | - | else: | |
| 631 | - | url = urlparse(url) | |
| 632 | - | ||
| 633 | - | if url.scheme not in cls.CACHE_SCHEMES: | |
| 634 | - | raise ImproperlyConfigured( | |
| 635 | - | 'Invalid cache schema {}'.format(url.scheme) | |
| 636 | - | ) | |
| 637 | - | ||
| 638 | - | location = url.netloc.split(',') | |
| 639 | - | if len(location) == 1: | |
| 640 | - | location = location[0] | |
| 641 | - | ||
| 642 | - | config = { | |
| 643 | - | 'BACKEND': cls.CACHE_SCHEMES[url.scheme], | |
| 644 | - | 'LOCATION': location, | |
| 645 | - | } | |
| 646 | - | ||
| 647 | - | # Add the drive to LOCATION | |
| 648 | - | if url.scheme == 'filecache': | |
| 649 | - | config.update({ | |
| 650 | - | 'LOCATION': url.netloc + url.path, | |
| 651 | - | }) | |
| 652 | - | ||
| 653 | - | # urlparse('pymemcache://127.0.0.1:11211') | |
| 654 | - | # => netloc='127.0.0.1:11211', path='' | |
| 655 | - | # | |
| 656 | - | # urlparse('pymemcache://memcached:11211/?key_prefix=ci') | |
| 657 | - | # => netloc='memcached:11211', path='/' | |
| 658 | - | # | |
| 659 | - | # urlparse('memcache:///tmp/memcached.sock') | |
| 660 | - | # => netloc='', path='/tmp/memcached.sock' | |
| 661 | - | if not url.netloc and url.scheme in ['memcache', 'pymemcache']: | |
| 662 | - | config.update({ | |
| 663 | - | 'LOCATION': 'unix:' + url.path, | |
| 664 | - | }) | |
| 665 | - | elif url.scheme.startswith('redis'): | |
| 666 | - | if url.hostname: | |
| 667 | - | scheme = url.scheme.replace('cache', '') | |
| 668 | - | else: | |
| 669 | - | scheme = 'unix' | |
| 670 | - | locations = [scheme + '://' + loc + url.path | |
| 671 | - | for loc in url.netloc.split(',')] | |
| 672 | - | if len(locations) == 1: | |
| 673 | - | config['LOCATION'] = locations[0] | |
| 674 | - | else: | |
| 675 | - | config['LOCATION'] = locations | |
| 676 | - | ||
| 677 | - | if url.query: | |
| 678 | - | config_options = {} | |
| 679 | - | for k, v in parse_qs(url.query).items(): | |
| 680 | - | opt = {k.upper(): _cast(v[0])} | |
| 681 | - | if k.upper() in cls._CACHE_BASE_OPTIONS: | |
| 682 | - | config.update(opt) | |
| 683 | - | else: | |
| 684 | - | config_options.update(opt) | |
| 685 | - | config['OPTIONS'] = config_options | |
| 686 | - | ||
| 687 | - | if backend: | |
| 688 | - | config['BACKEND'] = backend | |
| 689 | - | ||
| 690 | - | return config | |
| 691 | - | ||
| 692 | - | @classmethod | |
| 693 | - | def email_url_config(cls, url, backend=None): | |
| 694 | - | """Parse an arbitrary email URL. | |
| 695 | - | ||
| 696 | - | :param urllib.parse.ParseResult or str url: | |
| 697 | - | Email URL to parse. | |
| 698 | - | :param str or None backend: | |
| 699 | - | If None, the backend is evaluates from the ``url``. | |
| 700 | - | :return: Parsed email URL. | |
| 701 | - | :rtype: dict | |
| 702 | - | """ | |
| 703 | - | ||
| 704 | - | config = {} | |
| 705 | - | ||
| 706 | - | url = urlparse(url) if not isinstance(url, cls.URL_CLASS) else url | |
| 707 | - | ||
| 708 | - | # Remove query strings | |
| 709 | - | path = url.path[1:] | |
| 710 | - | path = unquote_plus(path.split('?', 2)[0]) | |
| 711 | - | ||
| 712 | - | # Update with environment configuration | |
| 713 | - | config.update({ | |
| 714 | - | 'EMAIL_FILE_PATH': path, | |
| 715 | - | 'EMAIL_HOST_USER': _cast_urlstr(url.username), | |
| 716 | - | 'EMAIL_HOST_PASSWORD': _cast_urlstr(url.password), | |
| 717 | - | 'EMAIL_HOST': url.hostname, | |
| 718 | - | 'EMAIL_PORT': _cast_int(url.port), | |
| 719 | - | }) | |
| 720 | - | ||
| 721 | - | if backend: | |
| 722 | - | config['EMAIL_BACKEND'] = backend | |
| 723 | - | elif url.scheme not in cls.EMAIL_SCHEMES: | |
| 724 | - | raise ImproperlyConfigured('Invalid email schema %s' % url.scheme) | |
| 725 | - | elif url.scheme in cls.EMAIL_SCHEMES: | |
| 726 | - | config['EMAIL_BACKEND'] = cls.EMAIL_SCHEMES[url.scheme] | |
| 727 | - | ||
| 728 | - | if url.scheme in ('smtps', 'smtp+tls'): | |
| 729 | - | config['EMAIL_USE_TLS'] = True | |
| 730 | - | elif url.scheme == 'smtp+ssl': | |
| 731 | - | config['EMAIL_USE_SSL'] = True | |
| 732 | - | ||
| 733 | - | if url.query: | |
| 734 | - | config_options = {} | |
| 735 | - | for k, v in parse_qs(url.query).items(): | |
| 736 | - | opt = {k.upper(): _cast_int(v[0])} | |
| 737 | - | if k.upper() in cls._EMAIL_BASE_OPTIONS: | |
| 738 | - | config.update(opt) | |
| 739 | - | else: | |
| 740 | - | config_options.update(opt) | |
| 741 | - | config['OPTIONS'] = config_options | |
| 742 | - | ||
| 743 | - | return config | |
| 744 | - | ||
| 745 | - | @classmethod | |
| 746 | - | def search_url_config(cls, url, engine=None): | |
| 747 | - | """Parse an arbitrary search URL. | |
| 748 | - | ||
| 749 | - | :param urllib.parse.ParseResult or str url: | |
| 750 | - | Search URL to parse. | |
| 751 | - | :param str or None engine: | |
| 752 | - | If None, the engine is evaluates from the ``url``. | |
| 753 | - | :return: Parsed search URL. | |
| 754 | - | :rtype: dict | |
| 755 | - | """ | |
| 756 | - | ||
| 757 | - | config = {} | |
| 758 | - | ||
| 759 | - | url = urlparse(url) if not isinstance(url, cls.URL_CLASS) else url | |
| 760 | - | ||
| 886 further changed lines not shown | |||
The check that tells the two apart
fail→pass·tests/test_path.py::test_comparison
Check file tests/test_path.py, taken without changes from the fix and copied onto the older code, so the exact same check runs against both versions.
Origin and history
The code before itea308232d474ad1eb7fc6826869baad6b7136b96
Broken version dated2022-06-14
Moduleenviron.environ
Units changedEnv, Path
Fingerprint2690f44ddd4d41b7
Checked2026-08-18 by goldset/0.1
Every field above is generated by our program. None of it is written by hand.
Other bugs found in joke2k/django-environ
- 2026-02-20Fix prometheus DB scheme aliases: underscores → hyphens (#585)
- 2026-02-17Feature/add choice parameter and raise an exception if fetched value is not within (#555)
- 2021-12-13fix unquote vs unquote_plus issue
- 2021-12-09fix[db_url_config]: added postgres cluster DSN support (ie postgresql://username:password@host1:port1,host2:port2/database)
- 2021-09-01Fix db_url_config to work the same for all postgres-like schemes