Repository: reata/sqllineage
Pull request #553 (2024-01-15 03:19:24)

fix: Set param from config
Dear ALL：

目前模型的默认模式名是通过环境变量传递的，我认为实现方法不够python。
所以，我实现了另一种解决方法。

修改 sqllineage/config.py 实现配置项的读取和写入， 读取方法保持和已有方法兼容。
修改 sqllineage/core/models.py 的Schema 类，在每次初始化的时候动态读取配置
修改 sqllineage/runner.py的init方法，传入变量，并在 SQLLineageConfig 赋值
请大家提出意见，谢谢
Currently the default schema name of the model is passed through environment variables, and I think the implementation method is not python enough.
So, I implemented another workaround.

Modify sqllineage/config.py to implement reading and writing of configuration items. The reading method remains compatible with the existing method.
Modify the Schema class of sqllineage/core/models.py and dynamically read the configuration every time it is initialized.
Modify the init method of sqllineage/runner.py, pass in variables, and assign values in SQLLineageConfig
Please give your opinions, thank you



重新建立分支并提交
Re-branch and submit

first step to solve #536 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/reata/sqllineage/pull/553?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`06a474b`)](https://app.codecov.io/gh/reata/sqllineage/commit/06a474b5e7d6a26994db18f97cf9058246cd24bd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 99.50% compared to head [(`012b690`)](https://app.codecov.io/gh/reata/sqllineage/pull/553?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 99.32%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #553      +/-   ##
==========================================
- Coverage   99.50%   99.32%   -0.18%     
==========================================
  Files          41       41              
  Lines        2211     2221      +10     
==========================================
+ Hits         2200     2206       +6     
- Misses         11       15       +4     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/reata/sqllineage/pull/553?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

reata: Do we have all the codes in this new branch? I remember we need to change the Schema model in #545 
delphisharp: > Do we have all the codes in this new branch? I remember we need to change the Schema model in #545

Sorry, there were some problems when my local branch was synchronized from upstream changes of the master. 
So a new branch was reopened.
reata: What's the difference between this and #555 ? Do they jointly solve #536 and is there any dependency, like I need to merge this PR first and then #555?

Patch:
diff --git a/sqllineage/config.py b/sqllineage/config.py
index 6e75ac6..b4d83e0 100644
--- a/sqllineage/config.py
+++ b/sqllineage/config.py
@@ -1,4 +1,8 @@
 import os
+import threading
+from typing import Any, Dict, Set
+
+from sqllineage.exceptions import ConfigException
 
 
 class _SQLLineageConfigLoader:
@@ -17,10 +21,18 @@ class _SQLLineageConfigLoader:
         # lateral column alias reference supported by some dialect (redshift, spark 3.4+, etc)
         "LATERAL_COLUMN_ALIAS_REFERENCE": (bool, False),
     }
-    BOOLEAN_TRUE_STRINGS = ("true", "on", "ok", "y", "yes", "1")
 
-    def __getattr__(self, item):
-        if item in self.config:
+    def __init__(self) -> None:
+        self._thread_config: Dict[int, Dict[str, Any]] = {}
+        self._thread_in_context_manager: Set[int] = set()
+
+    def __getattr__(self, item: str):
+        if item in self.config.keys():
+            if (
+                value := self._thread_config.get(self.get_ident(), {}).get(item)
+            ) is not None:
+                return value
+
             type_, default = self.config[item]
             # require SQLLINEAGE_ prefix from environment variable
             return self.parse_value(
@@ -29,23 +41,57 @@ class _SQLLineageConfigLoader:
         else:
             return super().__getattribute__(item)
 
-    @classmethod
-    def parse_value(cls, value, cast):
-        """Parse and cast provided value
+    def __setattr__(self, key, value) -> None:
+        if key in self.config:
+            raise ConfigException(
+                "SQLLineageConfig is read-only. Use context manager to update thread level config."
+            )
+        else:
+            super().__setattr__(key, value)
+
+    def __call__(self, *args, **kwargs):
+        if self.get_ident() not in self._thread_config.keys():
+            self._thread_config[self.get_ident()] = {}
+        for key, value in kwargs.items():
+            if key in self.config.keys():
+                self._thread_config[self.get_ident()][key] = self.parse_value(
+                    value, self.config[key][0]
+                )
+            else:
+                raise ConfigException(f"Invalid config key: {key}")
+        return self
 
+    def __enter__(self):
+        if (thread_id := self.get_ident()) not in self._thread_in_context_manager:
+            self._thread_in_context_manager.add(thread_id)
+        else:
+            raise ConfigException("SQLLineageConfig context manager is not reentrant")
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        thread_id = self.get_ident()
+        if thread_id in self._thread_config:
+            self._thread_config.pop(self.get_ident())
+        if thread_id in self._thread_in_context_manager:
+            self._thread_in_context_manager.remove(thread_id)
+
+    @staticmethod
+    def get_ident() -> int:
+        return threading.get_ident()
+
+    @staticmethod
+    def parse_value(value, cast) -> Any:
+        """Parse and cast provided value
         :param value: Stringed value.
         :param cast: Type to cast return value as.
-
-        :returns: Casted value
+        :returns: cast value
         """
         if cast is bool:
             try:
                 value = int(value) != 0
             except ValueError:
-                value = value.lower().strip() in cls.BOOLEAN_TRUE_STRINGS
+                value = value.lower().strip() in ("true", "on", "ok", "y", "yes", "1")
         else:
             value = cast(value)
-
         return value
 
 
diff --git a/sqllineage/exceptions.py b/sqllineage/exceptions.py
index 99d42ca..3b87d9f 100644
--- a/sqllineage/exceptions.py
+++ b/sqllineage/exceptions.py
@@ -12,3 +12,7 @@ class InvalidSyntaxException(SQLLineageException):
 
 class MetaDataProviderException(SQLLineageException):
     """Raised for MetaDataProvider errors"""
+
+
+class ConfigException(SQLLineageException):
+    """Raised for configuration errors"""
diff --git a/sqllineagejs/package-lock.json b/sqllineagejs/package-lock.json
index fec8409..d4743e9 100644
--- a/sqllineagejs/package-lock.json
+++ b/sqllineagejs/package-lock.json
@@ -5403,13 +5403,13 @@
       "dev": true
     },
     "node_modules/body-parser": {
-      "version": "1.20.1",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
-      "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+      "version": "1.20.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
+      "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
       "dev": true,
       "dependencies": {
         "bytes": "3.1.2",
-        "content-type": "~1.0.4",
+        "content-type": "~1.0.5",
         "debug": "2.6.9",
         "depd": "2.0.0",
         "destroy": "1.2.0",
@@ -5417,7 +5417,7 @@
         "iconv-lite": "0.4.24",
         "on-finished": "2.4.1",
         "qs": "6.11.0",
-        "raw-body": "2.5.1",
+        "raw-body": "2.5.2",
         "type-is": "~1.6.18",
         "unpipe": "1.0.0"
       },
@@ -6019,9 +6019,9 @@
       ]
     },
     "node_modules/content-type": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
-      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
       "dev": true,
       "engines": {
         "node": ">= 0.6"
@@ -6037,9 +6037,9 @@
       }
     },
     "node_modules/cookie": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
-      "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+      "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
       "dev": true,
       "engines": {
         "node": ">= 0.6"
@@ -8001,17 +8001,17 @@
       }
     },
     "node_modules/express": {
-      "version": "4.18.2",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
-      "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+      "version": "4.19.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
+      "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
       "dev": true,
       "dependencies": {
         "accepts": "~1.3.8",
         "array-flatten": "1.1.1",
-        "body-parser": "1.20.1",
+        "body-parser": "1.20.2",
         "content-disposition": "0.5.4",
         "content-type": "~1.0.4",
-        "cookie": "0.5.0",
+        "cookie": "0.6.0",
         "cookie-signature": "1.0.6",
         "debug": "2.6.9",
         "depd": "2.0.0",
@@ -8449,9 +8449,9 @@
       "dev": true
     },
     "node_modules/follow-redirects": {
-      "version": "1.15.4",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
-      "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
+      "version": "1.15.6",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+      "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
       "dev": true,
       "funding": [
         {
@@ -8719,9 +8719,9 @@
       }
     },
     "node_modules/fs-monkey": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
-      "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz",
+      "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==",
       "dev": true
     },
     "node_modules/fs.realpath": {
@@ -12936,12 +12936,12 @@
       }
     },
     "node_modules/memfs": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
-      "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz",
+      "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
       "dev": true,
       "dependencies": {
-        "fs-monkey": "1.0.3"
+        "fs-monkey": "^1.0.4"
       },
       "engines": {
         "node": ">= 4.0.0"
@@ -15384,9 +15384,9 @@
       }
     },
     "node_modules/raw-body": {
-      "version": "2.5.1",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
-      "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
       "dev": true,
       "dependencies": {
         "bytes": "3.1.2",
@@ -18033,13 +18033,13 @@
       }
     },
     "node_modules/webpack-dev-middleware": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz",
-      "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==",
+      "version": "5.3.4",
+      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz",
+      "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==",
       "dev": true,
       "dependencies": {
         "colorette": "^2.0.10",
-        "memfs": "^3.4.1",
+        "memfs": "^3.4.3",
         "mime-types": "^2.1.31",
         "range-parser": "^1.2.1",
         "schema-utils": "^4.0.0"
@@ -22807,13 +22807,13 @@
       "dev": true
     },
     "body-parser": {
-      "version": "1.20.1",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
-      "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+      "version": "1.20.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
+      "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
       "dev": true,
       "requires": {
         "bytes": "3.1.2",
-        "content-type": "~1.0.4",
+        "content-type": "~1.0.5",
         "debug": "2.6.9",
         "depd": "2.0.0",
         "destroy": "1.2.0",
@@ -22821,7 +22821,7 @@
         "iconv-lite": "0.4.24",
         "on-finished": "2.4.1",
         "qs": "6.11.0",
-        "raw-body": "2.5.1",
+        "raw-body": "2.5.2",
         "type-is": "~1.6.18",
         "unpipe": "1.0.0"
       },
@@ -23282,9 +23282,9 @@
       }
     },
     "content-type": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
-      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
       "dev": true
     },
     "convert-source-map": {
@@ -23297,9 +23297,9 @@
       }
     },
     "cookie": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
-      "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+      "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
       "dev": true
     },
     "cookie-signature": {
@@ -24767,17 +24767,17 @@
       }
     },
     "express": {
-      "version": "4.18.2",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
-      "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+      "version": "4.19.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
+      "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
       "dev": true,
       "requires": {
         "accepts": "~1.3.8",
         "array-flatten": "1.1.1",
-        "body-parser": "1.20.1",
+        "body-parser": "1.20.2",
         "content-disposition": "0.5.4",
         "content-type": "~1.0.4",
-        "cookie": "0.5.0",
+        "cookie": "0.6.0",
         "cookie-signature": "1.0.6",
         "debug": "2.6.9",
         "depd": "2.0.0",
@@ -25123,9 +25123,9 @@
       "dev": true
     },
     "follow-redirects": {
-      "version": "1.15.4",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
-      "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
+      "version": "1.15.6",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+      "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
       "dev": true
     },
     "for-each": {
@@ -25301,9 +25301,9 @@
       }
     },
     "fs-monkey": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
-      "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz",
+      "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==",
       "dev": true
     },
     "fs.realpath": {
@@ -28481,12 +28481,12 @@
       "dev": true
     },
     "memfs": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
-      "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz",
+      "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
       "dev": true,
       "requires": {
-        "fs-monkey": "1.0.3"
+        "fs-monkey": "^1.0.4"
       }
     },
     "merge-descriptors": {
@@ -30139,9 +30139,9 @@
       "dev": true
     },
     "raw-body": {
-      "version": "2.5.1",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
-      "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
       "dev": true,
       "requires": {
         "bytes": "3.1.2",
@@ -32150,13 +32150,13 @@
       }
     },
     "webpack-dev-middleware": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz",
-      "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==",
+      "version": "5.3.4",
+      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz",
+      "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==",
       "dev": true,
       "requires": {
         "colorette": "^2.0.10",
-        "memfs": "^3.4.1",
+        "memfs": "^3.4.3",
         "mime-types": "^2.1.31",
         "range-parser": "^1.2.1",
         "schema-utils": "^4.0.0"
diff --git a/tox.ini b/tox.ini
index d416383..f8f7346 100644
--- a/tox.ini
+++ b/tox.ini
@@ -14,7 +14,7 @@ commands =
 [flake8]
 exclude = .tox,.git,__pycache__,build,sqllineagejs,venv,env
 max-line-length = 120
-# ignore = D100,D101
+ignore = A005,W503
 show-source = true
 enable-extensions=G
 application-import-names = sqllineage


Test patch:
diff --git a/tests/core/test_config.py b/tests/core/test_config.py
index faf09d0..9d7ed46 100644
--- a/tests/core/test_config.py
+++ b/tests/core/test_config.py
@@ -1,7 +1,13 @@
+import concurrent.futures
 import os
+import random
+import time
 from unittest.mock import patch
 
+import pytest
+
 from sqllineage.config import SQLLineageConfig
+from sqllineage.exceptions import ConfigException
 
 
 @patch(
@@ -21,3 +27,53 @@ def test_config():
 
     assert type(SQLLineageConfig.TSQL_NO_SEMICOLON) is bool
     assert SQLLineageConfig.TSQL_NO_SEMICOLON is True
+
+
+def test_disable_direct_update_config():
+    with pytest.raises(ConfigException):
+        SQLLineageConfig.DEFAULT_SCHEMA = "ods"
+
+
+def test_update_config_using_context_manager():
+    with SQLLineageConfig(LATERAL_COLUMN_ALIAS_REFERENCE=True):
+        assert SQLLineageConfig.LATERAL_COLUMN_ALIAS_REFERENCE is True
+    assert SQLLineageConfig.LATERAL_COLUMN_ALIAS_REFERENCE is False
+
+    with SQLLineageConfig(DEFAULT_SCHEMA="ods"):
+        assert SQLLineageConfig.DEFAULT_SCHEMA == "ods"
+    assert SQLLineageConfig.DEFAULT_SCHEMA == ""
+
+    with SQLLineageConfig(DIRECTORY=""):
+        assert SQLLineageConfig.DIRECTORY == ""
+    assert SQLLineageConfig.DIRECTORY != ""
+
+
+def test_update_config_context_manager_non_reentrant():
+    with pytest.raises(ConfigException):
+        with SQLLineageConfig(DEFAULT_SCHEMA="ods"):
+            with SQLLineageConfig(DEFAULT_SCHEMA="dwd"):
+                pass
+
+
+def test_disable_update_unknown_config():
+    with pytest.raises(ConfigException):
+        with SQLLineageConfig(UNKNOWN_KEY="value"):
+            pass
+
+
+def _check_schema(schema: str):
+    # used by test_config_parallel, must be a global function so that it can be pickled between processes
+    with SQLLineageConfig(DEFAULT_SCHEMA=schema):
+        # randomly sleep [0, 0.1) second to simulate real parsing scenario
+        time.sleep(random.random() * 0.1)
+        return SQLLineageConfig.DEFAULT_SCHEMA
+
+
+@pytest.mark.parametrize("pool", ["ThreadPoolExecutor", "ProcessPoolExecutor"])
+def test_config_parallel(pool: str):
+    executor_class = getattr(concurrent.futures, pool)
+    schemas = [f"db{i}" for i in range(100)]
+    with executor_class() as executor:
+        futures = [executor.submit(_check_schema, schema) for schema in schemas]
+        for i, future in enumerate(futures):
+            assert future.result() == schemas[i]