Allow specification of logging.Logger re-exports via logger-objects (#5750)

## Summary

This PR adds a `logger-objects` setting that allows users to mark
specific symbols a `logging.Logger` objects. Currently, if a `logger` is
imported, we only flagged it as a `logging.Logger` if it comes exactly
from the `logging` module or is `flask.current_app.logger`.

This PR allows users to mark specific loggers, like
`logging_setup.logger`, to ensure that they're covered by the
`flake8-logging-format` rules and others.

For example, if you have a module `logging_setup.py` with the following
contents:

```python
import logging

logger = logging.getLogger(__name__)
```

Adding `"logging_setup.logger"` to `logger-objects` will ensure that
`logging_setup.logger` is treated as a `logging.Logger` object when
imported from other modules (e.g., `from logging_setup import logger`).

Closes https://github.com/astral-sh/ruff/issues/5694.
This commit is contained in:
Charlie Marsh 2023-07-24 00:38:20 -04:00 committed by GitHub
parent 727153cf45
commit f9726af4ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 197 additions and 104 deletions

View file

@ -1,7 +1,7 @@
use rustpython_parser::ast::{self, Constant, Expr, Keyword};
use rustpython_parser::ast::{self, Expr, Keyword};
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast::call_path::{collect_call_path, from_qualified_name};
use ruff_python_ast::helpers::{find_keyword, is_const_true};
use crate::model::SemanticModel;
@ -9,37 +9,53 @@ use crate::model::SemanticModel;
/// `logging.error`, `logger.error`, `self.logger.error`, etc., but not
/// arbitrary `foo.error` calls.
///
/// It even matches direct `logging.error` calls even if the `logging` module
/// It also matches direct `logging.error` calls when the `logging` module
/// is aliased. Example:
/// ```python
/// import logging as bar
///
/// # This is detected to be a logger candidate
/// # This is detected to be a logger candidate.
/// bar.error()
/// ```
pub fn is_logger_candidate(func: &Expr, semantic: &SemanticModel) -> bool {
if let Expr::Attribute(ast::ExprAttribute { value, .. }) = func {
let Some(call_path) = (if let Some(call_path) = semantic.resolve_call_path(value) {
if call_path
.first()
.map_or(false, |module| *module == "logging")
|| call_path.as_slice() == ["flask", "current_app", "logger"]
{
Some(call_path)
} else {
None
}
} else {
collect_call_path(value)
}) else {
return false;
};
pub fn is_logger_candidate(
func: &Expr,
semantic: &SemanticModel,
logger_objects: &[String],
) -> bool {
let Expr::Attribute(ast::ExprAttribute { value, .. }) = func else {
return false;
};
// If the symbol was imported from another module, ensure that it's either a user-specified
// logger object, the `logging` module itself, or `flask.current_app.logger`.
if let Some(call_path) = semantic.resolve_call_path(value) {
if matches!(
call_path.as_slice(),
["logging"] | ["flask", "current_app", "logger"]
) {
return true;
}
if logger_objects
.iter()
.any(|logger| from_qualified_name(logger) == call_path)
{
return true;
}
return false;
}
// Otherwise, if the symbol was defined in the current module, match against some common
// logger names.
if let Some(call_path) = collect_call_path(value) {
if let Some(tail) = call_path.last() {
if tail.starts_with("log") || tail.ends_with("logger") || tail.ends_with("logging") {
return true;
}
}
}
false
}
@ -49,23 +65,20 @@ pub fn exc_info<'a>(keywords: &'a [Keyword], semantic: &SemanticModel) -> Option
let exc_info = find_keyword(keywords, "exc_info")?;
// Ex) `logging.error("...", exc_info=True)`
if matches!(
exc_info.value,
Expr::Constant(ast::ExprConstant {
value: Constant::Bool(true),
..
})
) {
if is_const_true(&exc_info.value) {
return Some(exc_info);
}
// Ex) `logging.error("...", exc_info=sys.exc_info())`
if let Expr::Call(ast::ExprCall { func, .. }) = &exc_info.value {
if semantic.resolve_call_path(func).map_or(false, |call_path| {
if exc_info
.value
.as_call_expr()
.and_then(|call| semantic.resolve_call_path(&call.func))
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["sys", "exc_info"])
}) {
return Some(exc_info);
}
})
{
return Some(exc_info);
}
None