vault backup: 2024-01-01 17:19:42

This commit is contained in:
2024-01-01 17:19:42 +08:00
parent 0b2e0f0df9
commit 8aedf525b5
439 changed files with 39 additions and 39 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
一個範例:
```python
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-u", "--update_tool", default=WIN_FW_UPDATER_PATH, help="The path of win_fw_updater.exe.")
parser.add_argument("-f", "--firmware", required=True, help="The path of ITB file.")
parser.add_argument("-w", "--waittime_update_firmware", default=600, type=int, help="Wait time for update the firmware.")
parser.add_argument("-g", "--ignore_check_file_path", action='store_true', help="Skip check the existence of file.")
args = parser.parse_args()
```
#### 要求user一定要設定的參數
使用`required=True`
例如:
```python
parser.add_argument("-f", "--firmware", required=True, help="The path of ITB file.")
```
如果使用者沒有下`-f`(或者`--firmware=XXX`)就會報錯,如下:
```bash
FwUpdateCheck.py: error: the following arguments are required: -f/--firmware
```
#### 有設定才會產生的參數
使用`action='store_true'``action='store_false'`
例如:
```python
parser.add_argument("-g", "--ignore_check_file_path", action='store_true', help="Skip check the existence of file.")
```
當使用者沒有設置`-g`時,`args.ignore_check_file_path``False`,當設置時,`args.ignore_check_file_path``True`
#### 使用預設值
例如:
```python
parser.add_argument("-u", "--update_tool", default="C:\\tool.exe", help="The path of win_fw_updater.exe.")
```
`default=<Something>`來設定參數的預設值,上面的例子中,`args.update_tool`的預設值為`C:\tool.exe`
另外可以用`type=<Object type>`來指定預設值的型別。例如:
```python
parser.add_argument("-n", "--number", default=50, type=int, help="Assign a number")
```
上例中,`args.number`的預設值是50型別是`int`,所以可以直接運算,不需要再經過`int(args.number)`這樣的轉換。
#### 限制使用者的選擇
Example:
```python
parser.add_argument('move', choices=['rock', 'paper', 'scissors'])
```
使用`choices=<list>`來限定輸入的選項,上例中,使用者只能輸入'rock'、'paper'、'scissors'這三個字串中的其中一個,否則會報錯:
```bash
error: argument move: invalid choice: 'fire' (choose from 'rock', 'paper', 'scissors')
```
-----
- https://docs.python.org/zh-tw/3/library/argparse.html

View File

@@ -0,0 +1,5 @@
從list中選出n個項目有可能重複
```python
import random
random.choices(seq, n)
```

View File

@@ -0,0 +1,48 @@
## 在decorator內取得function的default argument與class member
```python
import sys
import inspect
from functools import wraps
def exampleDecorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Decorator: call by {func.__name__}")
def get_default_args(func):
signature = inspect.signature(func)
return {
k: v.default for k, v in signature.parameters.items() if v.default is not inspect.Parameter.empty
}
## Get default
defaultKwargs = get_default_args(func)
defaultKwargs.update(kwargs)
print(f"Decorator: args = {args}, kwargs = {kwargs}, defaultKwargs = {defaultKwargs}")
objectInstance = args[0]
if hasattr(objectInstance, 'defaultArg1'):
print(f'objectInstance has defaultArg1, a.defaultArg1({type(objectInstance.defaultArg1)}) = {objectInstance.defaultArg1}')
if objectInstance.defaultArg1:
## Do something here
print("Decorator: some message...")
else:
print('objectInstance does not have defaultArg1')
return func(*args, **kwargs)
return wrapper
class ExampleClass():
def __init__(self, defaultArg1=True, defaultArg2="SomeString"):
self.defaultArg1 = defaultArg1
self.defaultArg2 = defaultArg2
print(f'self.defaultArg1 = {self.defaultArg1}, self.defaultArg2 = {self.defaultArg2}')
@exampleDecorator
def run(self, arg1=1, arg2=2):
print(f"ExampleClass.run(), arg1 = {arg1}, arg2 = {arg2}")
example = ExampleClass()
example.run()
```

View File

@@ -0,0 +1,4 @@
- [Python 中的 Log 利器:使用 logging 模組來整理 print 訊息 - zhung to be lazy…](https://zhung.com.tw/article/python%E4%B8%AD%E7%9A%84log%E5%88%A9%E5%99%A8-%E4%BD%BF%E7%94%A8logging%E6%A8%A1%E7%B5%84%E4%BE%86%E6%95%B4%E7%90%86print%E8%A8%8A%E6%81%AF/)\
- [[Python] logging 教學](https://zwindr.blogspot.com/2016/08/python-logging.html)
- [How can I color Python logging output? - Stack Overflow](https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output)
- [logging如何使用 logging 紀錄事件 - Andrew Li](https://orcahmlee.github.io/python/python-logging/)

View File

@@ -0,0 +1,126 @@
### 準備
```
import logging
```
### logging level
| level | level number | funtion |
|:---------|:-------------|:---------------------|
| NOTSET | 0 | |
| DEBUG | 10 | `logging.debug()` |
| INFO | 20 | `logging.info()` |
| WARNING | 30 | `logging.warning()` |
| ERROR | 40 | `logging.error()` |
| CRITICAL | 50 | `logging.critical()` |
```
import logging
LOG_FORMAT = '%(asctime)s %(levelname)s: %(message)s'
LOG_FILENAME = 'C:\\RobotRun\\Output\\RobotRunDocUpdater.log'
logging.basicConfig(level=logging.INFO, filename=LOG_FILENAME, filemode='a', format=LOG_FORMAT)
logging.info('logging start')
```
### Print Exception
`logging` 模組也提供可以紀錄完整的堆疊追蹤 (stack traces),若在 `logging.error()` 加上 `exc_info` 參數,並將該參數設為 `True`,就可以紀錄 Exception如下
```python
import logging
try:
x = 5 / 0
except:
logging.error("Catch an exception.", exc_info=True)
```
也可以使用`logging.exception("Catch an exception.")`,效果跟`logging.error("Catch an exception.", exc_info=True)`一樣。
### 自訂 logging 輸出格式
預設的訊息輸出格式只有 `levelname``name``message`,下面是其他相關的資訊:
| 格式化字串 | 說明 |
|:------------------|:---------------------------------------------------------------------|
| `%(asctime)s` | 日期時間, 格式為 `YYYY-MM-DD HH:mm:SS,ms`例如2018-12-13 17:20:30,567 |
| `%(filename)s` | 模組檔名 |
| `%(funcName)s` | 函數名稱 |
| `%(levelname)s` | 日誌的等級名稱 |
| `%(levelno)s` | 日誌的等級數值 |
| `%(lineno)d` | 呼叫日誌函數所在的行數 |
| `%(message)s` | 訊息 |
| `%(module)s` | 模組名稱 |
| `%(name)s` | logger 的名稱 |
| `%(pathname)s` | 檔案的完整路徑 (如果可用) |
| `%(process)d` | process ID (如果可用) |
| `%(thread)d` | 執行緒 ID (如果可用) |
| `%(threradName)s` | 執行緒名稱 |
例:
```python
FORMAT = '%(asctime)s %(levelname)s: %(message)s'
logging.basicConfig(level=logging.DEBUG, format=FORMAT)
logging.debug('debug message') --> 2018-12-13 17:40:34,604 DEBUG: debug message
```
### 儲存log
只要在 `logging.basicConfig()` 內的 `filename` 參數設定要儲存的日誌檔名,就可以將 logging 儲存:
```python
import logging
FORMAT = '%(asctime)s %(levelname)s: %(message)s'
logging.basicConfig(level=logging.DEBUG, filename='myLog.log', filemode='w', format=FORMAT)
logging.debug('debug message')
```
預設 `filemode` 參數是設為 `a`,代表 append (附加) 的意思每次執行程式時Logging 會將新的訊息加在舊的訊息後面,不會覆蓋舊的訊息。若要改成新訊息覆蓋就訊息,那可以將 `filemode` 參數設為 `w`,代表 write 的意思。
### 儲存log也輸出到console
`logging`有4個主要module
- Logger暴露了應用程式程式碼能直接使用的介面。
- Handler記錄器產生的日誌記錄傳送至合適的目的地。
- Filter提供了更好的粒度控制它可以決定輸出哪些日誌記錄。
- Formatter指明瞭最終輸出中日誌記錄的佈局。
#### Handler
其中`Handlers`有以下幾類:
1. `logging.StreamHandler` -> 控制檯輸出
使用這個Handler可以向類似與`sys.stdout`或者`sys.stderr`的任何檔案物件(file object)輸出資訊。
它的建構函式是: `StreamHandler([strm])` 其中`strm`引數是一個檔案物件。預設是`sys.stderr`
2. `logging.FileHandler` -> 檔案輸出
和StreamHandler類似用於向一個檔案輸出日誌資訊。不過`FileHandler`會幫你開啟這個檔案。
它的建構函式是:`FileHandler(filename[,mode])` filename是檔名必須指定一個檔名。 `mode`是檔案的開啟方式。預設是`'a'`,即新增到檔案末端。
3. `logging.handlers.RotatingFileHandler` -> 按照大小自動分割日誌檔案,一旦達到指定的大小重新生成檔案
這個Handler類似於上面的`FileHandler`但是它可以管理檔案大小。當檔案達到一定大小之後它會自動將當前日誌檔案改名然後建立一個新的同名日誌檔案繼續輸出。比如日誌檔案是chat.log。當chat.log達到指定的大小之後`RotatingFileHandler`自動把 檔案改名為chat.log.1。不過如果chat.log.1已經存在會先把chat.log.1重新命名為chat.log.2。
最後重新建立 chat.log繼續輸出日誌資訊。它的建構函式是`RotatingFileHandler(filename[, mode[, maxBytes[, backupCount]]])`,其中`filename``mode`兩個引數和FileHandler一樣。`maxBytes`用於指定日誌檔案的最大檔案大小。如果maxBytes為0意味著日誌檔案可以無限大這時上面描述的重新命名過程就不會發生。 `backupCount`用於指定保留的備份檔案的個數。比如如果指定為2當上面描述的重新命名過程發生時原有的chat.log.2並不會被更名,而是被刪除。
4. `logging.handlers.TimedRotatingFileHandler` -> 按照時間自動分割日誌檔案
這個Handler和`RotatingFileHandler`類似,不過,它沒有通過判斷檔案大小來決定何時重新建立日誌檔案,而是間隔一定時間就自動建立新的日誌檔案。重新命名的過程與`RotatingFileHandler`類似,不過新的檔案不是附加數字,而是當前時間。它的建構函式是:`TimedRotatingFileHandler( filename [,when [,interval [,backupCount]]])`,其中`filename`引數和`backupCount`引數和`RotatingFileHandler`具有相同的意義。`interval`是時間間隔。 `when`引數是一個字串。表示時間間隔的單位,不區分大小寫。它有以下取值: S 秒 M 分 H 小時 D 天 W 每星期(`interval==0`時代表星期一) midnight 每天凌晨。
#### Formatters
Formatters預設的時間格式為`%Y-%m-%d %H:%M:%S`
#### Example
新增2個handler一個輸出到螢幕上一個寫到檔案裡。寫到檔案裡的那個handler必須是`logging.handlers.RotatingFileHandler`超過1MB時會自動分割。
```python
import logging
import logging.handlers
logger = logging.getLogger(filename) # filename就是你要存log的檔名
shell_print = logging.StreamHandler() # 往螢幕上輸出
shell_print.setFormatter(format_str) # 設定螢幕上顯示的格式
file_print = logging.handlers.RotatingFileHandler(
filename=filename,
mode='a',
maxBytes=1024*1024,
backupCount=backCount,
encoding='utf-8')
file_print.setFormatter(format_str) # 設定檔案裡寫入的格式
logger.addHandler(sh) # 把物件加到logger裡
logger.addHandler(th)
```
-----
參考:
- [Python - 日誌 (logging) 模組](https://titangene.github.io/article/python-logging.html)
- [`logging` — Logging facility for Python](https://docs.python.org/3/library/logging.html#module-logging "logging: Flexible event logging system for applications.")

View File

@@ -0,0 +1,72 @@
## 基本折線圖
給2個list一個 x一個 y
```python
plt.clf() # 把圖清掉,變空白
plt.plot(xList, yList)
```
## XY軸標籤
```python
plt.xlabel(
'Focus setting', # 標籤
fontsize=15, # 字型大小
labelpad=10, # 標籤留白
color='red', # 文字顏色
rotation=90, # 文字旋轉角度
fontweight='bold', # 粗體
)
```
## 不要顯示軸的刻線
```python
plt.gca().axes.get_xaxis().set_visible(False)
```
## 畫2張圖
```python
figure, axis = plt.subplots(2, 2)
```
`plt.subplots()` 來指定要畫幾張圖,第一個參數是要有幾個 row第二個參數是要有幾個 column。
`axis` 會是一個 array可以用類似座標的方式來控制你要的圖例如
```python
axis[0, 0].set_title("Sine Function")
axis[0, 1].set_title("Cosine Function")
```
`figure` 則是指外圍的大圖。
## 畫2條線
```python
plt.plot(x, y1, label='sine curve',color='b')
plt.plot(x, y2, label='cosine curve',color='r')
```
## 畫大圖
```python
figure(figsize=(12, 9), dpi=120)
```
`12``9`指的是英吋,`dpi`是每英吋幾個點,所以就是`12*120``9*120`,也就是`1440x1080`
## 存檔
```python
plt.savefig(f'plot_{folder}.png')
```
## 註記annotation
```python
ax = plt.gca()
ax.annotate(
'local max', # 註記文字
xy=(xmax, ymax), # 點的座標
xytext=(xmax, ymax + 5), # 文字的座標
arrowprops=dict( # 箭頭的屬性
facecolor='black', # 顏色:黑色
shrink=0.05), #
)
```
官方說明:[matplotlib.axes.Axes.annotate — Matplotlib 3.7.1](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.annotate.html)
## 在X軸上畫一個範圍
```python
plt.gca().axvspan(startXPos, endXPos, alpha=0.2, color='red')
```

View File

@@ -0,0 +1,61 @@
### 將camera包裝成class
```python
class CameraCv(object):
def __init__(self, videoSource=0):
self.videoSource = videoSource
self.camera = None
self.cameraWidth = 0
self.cameraHeight = 0
self.cameraPreviewThreadHandle = None
self.cameraPreviewThreadStopEvent = threading.Event()
self.lastframeRGB = None
self.latestFrame = None
def start(self):
print("Open Camera")
self.camera = cv2.VideoCapture(self.videoSource, cv2.CAP_DSHOW)
if not self.camera.isOpened():
raise ValueError("Unable to open video source {}".format(self.videoSource))
# Get video source width and height
self.cameraWidth = self.camera.get(cv2.CAP_PROP_FRAME_WIDTH)
self.cameraHeight = self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT)
self.cameraPreviewThreadStopEvent.clear()
self.cameraPreviewThreadHandle = threading.Thread(target=self.collectFrame, daemon=True, args=())
self.cameraPreviewThreadHandle.start()
def stop(self):
print("Close Camera")
self.cameraPreviewThreadStopEvent.set()
if self.camera.isOpened():
self.camera.release()
cv2.destroyAllWindows()
def collectFrame(self):
while True:
ret, frame = self.camera.read()
if ret:
# Return a boolean success flag and the current frame converted to BGR
self.lastframeRGB = frame
self.latestFrame = ImageTk.PhotoImage(image=Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)))
if self.cameraPreviewThreadStopEvent.is_set():
break
time.sleep(0.016)
def draw(self, container):
if self.latestFrame is not None:
container.imgtk = self.latestFrame
container.configure(image=self.latestFrame)
def read(self):
return self.camera.read()
def getLastFrameRgb(self):
return self.lastframeRGB
def saveFrame(self, filepath):
cv2.imwrite(filepath, self.getLastFrameRgb())
```

View File

@@ -0,0 +1,7 @@
從一個list中選出n個不重複的項目
```python
import random
random.sample(seq, n)
```
不像 [[choices()]] 是會重複的。

View File

@@ -0,0 +1,49 @@
### subprocess.Popen
```python
import subprocess
process = subprocess.Popen(['echo', 'More output'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
stdout, stderr
```
Input arguments is a list.
Notice `communicate()` will **block** until process was finished.
And the output string `stdout` and `stderr` is of type `byte`. You can convert the output to `string` by:
```python
new_string = stdout.decode('utf-8')
```
or use `universal_newlines=True` in `subprocess.Popen()`. Example:
```python
process = subprocess.Popen(['ping', '-c 4', 'python.org'],
stdout=subprocess.PIPE,
universal_newlines=True)
```
The `.poll()` will return the exit code of process. If process is still running. `.poll()` will return `None`. Example:
```python
process = subprocess.Popen(['ping', '-c 4', 'python.org'], stdout=subprocess.PIPE, universal_newlines=True)
while True:
output = process.stdout.readline()
print(output.strip())
# Do something else
return_code = process.poll()
if return_code is not None:
print('RETURN CODE', return_code)
# Process has finished, read rest of the output
for output in process.stdout.readlines():
print(output.strip())
break
```
-----
參考:
- [docs.python.org: `subprocess.Popen`](https://docs.python.org/3/library/subprocess.html#subprocess.Popen)
### subprocess.run
`subprocess.run()``subprocess.Popen()`是一樣的行為,差別是`subprocess.run()`會在process執行完畢之後才return也就是說流程會被block住。
`subprocess.run()`會回傳一個型別是`subprocess.CompletedProcess`的object.
-----
參考:
- [docs.python.org: _class_ `subprocess.CompletedProcess`](https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess)

View File

@@ -0,0 +1,2 @@
- [Python 多執行緒 threading 模組平行化程式設計教學 - G. T. Wang](https://blog.gtwang.org/programming/python-threading-multithreaded-programming-tutorial/)
- [Python — 多線程. 介紹 | by Jease | Jease隨筆 | Medium](https://medium.com/jeasee%E9%9A%A8%E7%AD%86/python-%E5%A4%9A%E7%B7%9A%E7%A8%8B-eb36272e604b)

View File

@@ -0,0 +1,96 @@
### 把[[matplotlib]]包裝成獨立視窗
```python
class Plot2D(Frame):
def __init__(self, parent, dataCollector, **kwargs):
Frame.__init__(self, parent.mainWindow, **kwargs)
self.parent = parent
self.mainWindows = Toplevel(parent.mainWindow)
self.mainWindows.title("AF State")
self.figure = plt.Figure(figsize=(9,5), dpi=100)
self.figure.suptitle('AF value plot', fontsize=16)
self.ax = self.figure.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.figure, master=self.mainWindows)
self.canvas.get_tk_widget().pack(fill='both')
self.axline = None
self.dataCollector = dataCollector
self.dataCollector.start()
def close(self):
print("Plot2D close")
self.mainWindows.destroy()
self.dataCollector.stop()
self.dataCollector = None
def draw(self):
if self.dataCollector:
datax, datay = self.dataCollector.getPlotData()
self.ax.clear()
self.ax.set_xlabel('Last {} datas'.format(self.dataCollector.getDataLength()))
self.axline, = self.ax.plot(datax, datay)
self.canvas.draw()
def getWindow(self):
return self.mainWindows
def getLastData(self):
return self.dataCollector.getLastData()
```
其中這一行:
```python
self.mainWindows = Toplevel(parent.mainWindow)
```
是用來開一個新的視窗,其中的`parent.mainWindow`就是用`tk.TK()`所產生出來的root。
因為需要一直更新資料,所以需要的一個`DataCollector`來提供資料,`DataCollector`會提供畫圖需要的list
```python
datax, datay = self.dataCollector.getPlotData()
```
`DataCollector`的定義如下:
```python
class AfStateCollector(threading.Thread):
def __init__(self, dataLength=100, pollingInterval=0.033):
threading.Thread.__init__(self)
self.dataLength = dataLength
self.pollingInterval = pollingInterval
self.stopEvent = threading.Event()
self.data = []
self.xdata = []
def run(self):
while True:
if self.stopEvent.is_set():
break
afValue = self.readAf()
self.data.append(afValue)
self.xdata.append(len(self.xdata))
if len(self.data) > self.dataLength:
self.data = self.data[-self.dataLength:]
self.xdata = list(range(self.dataLength))
# print(f'afValue = {afValue}')
time.sleep(self.pollingInterval)
print("AfStateCollector stopped.")
def readAf(self):
ReadTestXUreg_cmd = "lvreg testxu read 10"
ReadTestXUreg_cmd_process = subprocess.Popen(ReadTestXUreg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
outstring, err = ReadTestXUreg_cmd_process.communicate()
outstring = outstring.strip().decode('utf-8')
outstring = int(outstring, 16)
outstring_H = (outstring & 0xFF00) / 256
outstring_L = outstring & 0xFF
outAFStat = int(outstring_L * 256 + outstring_H)
return outAFStat
```
- [Python GUI之tkinter視窗視窗教程大集合看這篇就夠了 - IT閱讀](https://www.itread01.com/content/1547705544.html)
- [【Python】改善 VideoCapture 的影像延遲 | 夏恩的程式筆記 - 點部落](https://dotblogs.com.tw/shaynling/2017/12/28/091936)
- [Displaying a video feed with OpenCV and Tkinter - PyImageSearch](https://www.pyimagesearch.com/2016/05/30/displaying-a-video-feed-with-opencv-and-tkinter/)

View File

@@ -0,0 +1,96 @@
## 單元測試
### [pytest](https://docs.pytest.org/en/7.1.x/)
Pytest 不僅可以幫助我們運行測試還可以幫助我們配置如何運行它們、運行哪些文件等等……Pytest 有一個配置文件 `pytest.ini`,您可以在其中描述它的配置,例如哪個版本應該是 Pytest 或者哪些是測試文件,例如下列。
```ini
# pytet.ini
[pytest]
minversion = 6.0
addopts = -ra -q — cov=src — cov-report=html
python_files = test_*.py
```
### [tox](https://tox.wiki/en/latest/)
Tox 是一個通用的virtualenv管理和測試命令行工具。
使用不同的 Python 版本和解釋器檢查您的包是否正確安裝
在每個環境中運行您的測試,配置您選擇的測試工具
作為持續集成服務器的前端,大大減少樣板文件並合併 CI 和基於 shell 的測試。
Tox 也有它的配置文件。
```ini
[tox]
isolated_build = True
envlist = py{38}
[testenv]
usedevelop = true
deps = -r src/requirements_dev.txt
```
## 程式檢查工具
用來檢查程式是否符合coding style、PEP8之類的規範
### [pylint](https://github.com/PyCQA/pylint)
Pylint config: create `.pylintrc` file
```
[MESSAGES CONTROL]
disable=
missing-docstring,
too-few-public-methods[REPORTS]
output-format=colorized
files-output=no
reports=no
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
```
### [flake8](https://github.com/pycqa/flake8)
Flake8 config: create `.flake8` file
```
[flake8]
ignore = E203, E266, E501, W503, F403, F401, E402
max-line-length = 120
max-complexity = 18
select = B,C,E,F,W,T4,B9
exclude =
.git,
tests
```
### [mypy](http://www.mypy-lang.org/)
## Git hook
### pre-commit
Pre-commit 是一個創建 git hook的framework以確保您的代碼編寫與您定義的代碼樣式相對應。
它會掃描您的原始碼並運行您將在預提交配置文件中定義的所有檢查器:`.pre-commit-config.yaml`
```
repos:
- repo: 'https://gitlab.com/pycqa/flake8'
rev: 3.8.2
hooks:
- id: flake8
name: Style Guide Enforcement (flake8)
args:
- '--max-line-length=120'
- repo: 'https://github.com/pre-commit/mirrors-mypy'
rev: v0.720
hooks:
- id: mypy
name: Optional Static Typing for Python (mypy)
```
## 漏洞檢查
### [SonarQube](https://www.sonarqube.org/)
有很多用於漏洞掃描的工具,但我們將看看[Sonarqube](https://www.sonarqube.org/)。Sonarqube 是用於代碼質量和安全掃描的開源強大工具,是該行業的領先工具之一。
更多在[官方文檔](https://docs.sonarqube.org/latest/)中。
您可以使用 Docker 映像設置本地 Sonarqube 服務器並定義`sonar-project.properties`
```
# must be unique in a given SonarQube instance
sonar.projectKey=python_app_blueprint
# --- optional properties ---
# defaults to project key
#sonar.projectName=My project
# defaults to 'not provided'
#sonar.projectVersion=1.0
# Path is relative to the sonar-project.properties file. Defaults to .
#sonar.sources=.
# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8
```