DS-4878 Pandas-specific-quick-fix-replace-listdf.col.values-to-df.col.tolist

Add new intention and a corresponding quick fix for the usage pd.Series.values property from pandas library.

^DS-4878 Fixed

Merge-request: IJ-MR-106089
Merged-by: Natalia Murycheva <natalia.murycheva@jetbrains.com>

GitOrigin-RevId: 0c8dc40b09ee2d95ecd8ded532f31f5ef4a7740f
This commit is contained in:
Natalia.Murycheva
2023-05-13 20:44:29 +00:00
committed by intellij-monorepo-bot
parent 3d5155fe07
commit 0d53aa47df
14 changed files with 494 additions and 0 deletions
@@ -0,0 +1,14 @@
<html>
<body>
<p>Reports redundant <code>list</code> in <code>list(Series.values)</code> statement for pandas and polars libraries.
Such <code>Series</code> values extraction can be replaced with the <code>to_list()</code> function call.</p>
<p><b>Example:</b></p>
<pre style="font-family: monospace">
list(df['column'].values)
</pre>
<p>When the quick-fix is applied, the code changes to:</p>
<pre style="font-family: monospace">
df['column'].to_list()
</pre>
</body>
</html>
@@ -1245,3 +1245,7 @@ INSP.class.var.can.not.override.instance.variable=Cannot override instance varia
INSP.class.var.can.not.be.used.in.annotations.for.function.parameters='ClassVar' cannot be used in annotations for function parameters
INSP.class.var.can.not.be.used.in.annotation.for.function.return.value='ClassVar' cannot be used in annotation for a function return value
INSP.class.var.can.not.include.type.variables='ClassVar' parameter cannot include type variables
# Pandas-Specific inspections and quick fixes
INSP.pandas.series.values.replace.with.tolist=Method Series.to_list() is recommended
QFIX.pandas.series.values.replace.with.tolist=Replace list(Series.values) with Series.to_list()
@@ -194,6 +194,9 @@
<localInspection language="Python" shortName="PyRelativeImportInspection" suppressId="PyPackages" bundle="messages.PyPsiBundle"
key="INSP.NAME.relative.import" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING"
implementationClass="com.jetbrains.python.inspections.PyRelativeImportInspection"/>
<localInspection language="Python" shortName="PyPandasSeriesToListInspection" suppressId="PyPackages" bundle="messages.PyPsiBundle"
key="INSP.pandas.series.values.replace.with.tolist" enabledByDefault="true" level="WARNING"
implementationClass="com.jetbrains.python.inspections.PyPandasSeriesToListInspection"/>
<defaultLiveTemplates file="liveTemplates/Python.xml"/>
<liveTemplateContext contextId="Python"
@@ -0,0 +1,88 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.types.PyClassType
import com.jetbrains.python.psi.types.PyType
import com.jetbrains.python.psi.types.TypeEvalContext
class PyPandasSeriesToListInspection : PyInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
return Visitor(holder, PyInspectionVisitor.getContext(session))
}
private class Visitor(holder: ProblemsHolder, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
override fun visitPyCallExpression(node: PyCallExpression) {
if ((PsiTreeUtil.hasErrorElements(node))) return
if (node.callee?.text != "list" || node.arguments.size != 1) return
val argument = node.arguments.single()
if (argument !is PyQualifiedExpression) return
if (argument.referencedName != "values") return
val qualifier = argument.qualifier ?: return
if (hasSeriesType(qualifier, myTypeEvalContext)) {
registerProblem(node, PyPsiBundle.message("INSP.pandas.series.values.replace.with.tolist"), DSPandasSeriesToListQuickFix())
}
}
/**
* Checks whether expression possibly has type Series.
*/
private fun hasSeriesType(expression: PyExpression, context: TypeEvalContext): Boolean {
if (expression is PySubscriptionExpression) {
return context.getType(expression.indexExpression).isPyClassWithName("str")
}
val expressionType = context.getType(expression)
if (expressionType.isPyClassWithName("Series")) return true
if (expressionType != null) return false
return isQualifiedDataframeCall(expression, context)
}
private fun isQualifiedDataframeCall(expression: PyExpression, context: TypeEvalContext): Boolean {
if (expression !is PyQualifiedExpression) return false
return context.getType(expression.qualifier).isPyClassWithName("DataFrame")
}
}
private class DSPandasSeriesToListQuickFix : LocalQuickFix {
override fun getFamilyName() = PyPsiBundle.message("QFIX.pandas.series.values.replace.with.tolist")
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val functionCall = descriptor.psiElement as? PyCallExpression ?: return
val argument = functionCall.arguments.singleOrNull() as? PyQualifiedExpression ?: return
val argumentQualifier = argument.qualifier ?: return
val toListCallString = argumentQualifier.text + ".to_list()"
val toListCallStatement = PyElementGenerator.getInstance(project).createFromText(
LanguageLevel.forElement(functionCall),
PyExpressionStatement::class.java,
toListCallString
)
functionCall.replace(toListCallStatement.expression)
}
}
}
private fun PyType?.isPyClassWithName(typeName: String) =
this is PyClassType && name == typeName
private fun TypeEvalContext.getType(element: PyTypedElement?): PyType? {
if (element == null) return null
return getType(element)
}
@@ -0,0 +1,5 @@
import pandas as pd
# DataFrame columns case
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
<warning descr="Method Series.to_list() is recommended">list<caret>(df.b.values)</warning>
@@ -0,0 +1,5 @@
import pandas as pd
# DataFrame columns case
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
df.b.to_list()
@@ -0,0 +1,12 @@
import pandas as pd
# DataFrame columns case
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
list(df[['a', 'b']].values)
bb = ["a", "b", "c"]
list(df[bb].values)
# with errors
list(df.<error descr="Name expected">[</error>'a'].values)
<warning descr="Method Series.to_list() is recommended">list<caret>(df['a'].values)</warning>
@@ -0,0 +1,12 @@
import pandas as pd
# DataFrame columns case
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
list(df[['a', 'b']].values)
bb = ["a", "b", "c"]
list(df[bb].values)
# with errors
list(df.['a'].values)
df['a'].to_list()
@@ -0,0 +1,2 @@
from series import Series
from frame import DataFrame
@@ -0,0 +1,18 @@
from series import Series
class NDFrame:
...
class DataFrame:
def __getitem__(self, key):
if key == "1":
return None
if key == "2":
return Series()
if key == "3":
return NDFrame()
if key == "4":
return DataFrame()
@@ -0,0 +1,292 @@
class property(object):
"""
Property attribute.
fget
function to be used for getting an attribute value
fset
function to be used for setting an attribute value
fdel
function to be used for del'ing an attribute
doc
docstring
Typical use is to define a managed attribute x:
class C(object):
def getx(self): return self._x
def setx(self, value): self._x = value
def delx(self): del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
Decorators make defining new properties or modifying existing ones easy:
class C(object):
@property
def x(self):
"I am the 'x' property."
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
"""
def deleter(self, *args, **kwargs): # real signature unknown
""" Descriptor to obtain a copy of the property with a different deleter. """
pass
def getter(self, *args, **kwargs): # real signature unknown
""" Descriptor to obtain a copy of the property with a different getter. """
pass
def setter(self, *args, **kwargs): # real signature unknown
""" Descriptor to obtain a copy of the property with a different setter. """
pass
def __delete__(self, *args, **kwargs): # real signature unknown
""" Delete an attribute of instance. """
pass
def __getattribute__(self, *args, **kwargs): # real signature unknown
""" Return getattr(self, name). """
pass
def __get__(self, *args, **kwargs): # real signature unknown
""" Return an attribute of instance, which is of type owner. """
pass
def __init__(self, fget=None, fset=None, fdel=None, doc=None): # known special case of property.__init__
"""
Property attribute.
fget
function to be used for getting an attribute value
fset
function to be used for setting an attribute value
fdel
function to be used for del'ing an attribute
doc
docstring
Typical use is to define a managed attribute x:
class C(object):
def getx(self): return self._x
def setx(self, value): self._x = value
def delx(self): del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
Decorators make defining new properties or modifying existing ones easy:
class C(object):
@property
def x(self):
"I am the 'x' property."
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
# (copied from class doc)
"""
pass
@staticmethod # known case of __new__
def __new__(*args, **kwargs): # real signature unknown
""" Create and return a new object. See help(type) for accurate signature. """
pass
def __set__(self, *args, **kwargs): # real signature unknown
""" Set an attribute of instance to value. """
pass
fdel = property(lambda self: object(), lambda self, v: None, lambda self: None) # default
fget = property(lambda self: object(), lambda self, v: None, lambda self: None) # default
fset = property(lambda self: object(), lambda self, v: None, lambda self: None) # default
__isabstractmethod__ = property(lambda self: object(), lambda self, v: None, lambda self: None) # default
class IndexOpsMixin():
"""
Common ops mixin to support a unified interface / docs for Series / Index
"""
def tolist(self):
"""
Return a list of the values.
These are each a scalar type, which is a Python scalar
(for str, int, float) or a pandas scalar
(for Timestamp/Timedelta/Interval/Period)
Returns
-------
list
See Also
--------
numpy.ndarray.tolist : Return the array as an a.ndim-levels deep
nested list of Python scalars.
"""
# return self._values.tolist()
...
to_list = tolist
class NDFrame:
...
class Series(IndexOpsMixin, NDFrame):
"""
One-dimensional ndarray with axis labels (including time series).
Labels need not be unique but must be a hashable type. The object
supports both integer- and label-based indexing and provides a host of
methods for performing operations involving the index. Statistical
methods from ndarray have been overridden to automatically exclude
missing data (currently represented as NaN).
Operations between Series (+, -, /, \\*, \\*\\*) align values based on their
associated index values-- they need not be the same length. The result
index will be the sorted union of the two indexes.
Parameters
----------
data : array-like, Iterable, dict, or scalar value
Contains data stored in Series. If data is a dict, argument order is
maintained.
index : array-like or Index (1d)
Values must be hashable and have the same length as `data`.
Non-unique index values are allowed. Will default to
RangeIndex (0, 1, 2, ..., n) if not provided. If data is dict-like
and index is None, then the keys in the data are used as the index. If the
index is not None, the resulting Series is reindexed with the index values.
dtype : str, numpy.dtype, or ExtensionDtype, optional
Data type for the output Series. If not specified, this will be
inferred from `data`.
See the :ref:`user guide <basics.dtypes>` for more usages.
name : str, optional
The name to give to the Series.
copy : bool, default False
Copy input data. Only affects Series or 1d ndarray input. See examples.
Examples
--------
Constructing Series from a dictionary with an Index specified
# >>> d = {'a': 1, 'b': 2, 'c': 3}
# >>> ser = pd.Series(data=d, index=['a', 'b', 'c'])
# >>> ser
a 1
b 2
c 3
dtype: int64
The keys of the dictionary match with the Index values, hence the Index
values have no effect.
# >>> d = {'a': 1, 'b': 2, 'c': 3}
# >>> ser = pd.Series(data=d, index=['x', 'y', 'z'])
# >>> ser
x NaN
y NaN
z NaN
dtype: float64
Note that the Index is first build with the keys from the dictionary.
After this the Series is reindexed with the given Index values, hence we
get all NaN as a result.
Constructing Series from a list with `copy=False`.
# >>> r = [1, 2]
# >>> ser = pd.Series(r, copy=False)
# >>> ser.iloc[0] = 999
# >>> r
[1, 2]
# >>> ser
0 999
1 2
dtype: int64
Due to input data type the Series has a `copy` of
the original data even though `copy=False`, so
the data is unchanged.
Constructing Series from a 1d ndarray with `copy=False`.
# >>> r = np.array([1, 2])
# >>> ser = pd.Series(r, copy=False)
# >>> ser.iloc[0] = 999
# >>> r
array([999, 2])
# >>> ser
0 999
1 2
dtype: int64
Due to input data type the Series has a `view` on
the original data, so
the data is changed as well.
"""
def __init__(
self,
data=None,
index=None,
dtype = None,
name=None,
copy = False,
fastpath = False,
):
...
@property
def values(self):
"""
Return Series as ndarray or ndarray-like depending on the dtype.
.. warning::
We recommend using :attr:`Series.array` or
:meth:`Series.to_numpy`, depending on whether you need
a reference to the underlying data or a NumPy array.
Returns
-------
numpy.ndarray or ndarray-like
See Also
--------
Series.array : Reference to the underlying data.
Series.to_numpy : A NumPy array representing the underlying data.
Examples
--------
# >>> pd.Series([1, 2, 3]).values
array([1, 2, 3])
# >>> pd.Series(list('aabc')).values
array(['a', 'a', 'b', 'c'], dtype=object)
# >>> pd.Series(list('aabc')).astype('category').values
['a', 'a', 'b', 'c']
Categories (3, object): ['a', 'b', 'c']
Timezone aware datetime data is converted to UTC:
# >>> pd.Series(pd.date_range('20130101', periods=3,
... tz='US/Eastern')).values
array(['2013-01-01T05:00:00.000000000',
'2013-01-02T05:00:00.000000000',
'2013-01-03T05:00:00.000000000'], dtype='datetime64[ns]')
"""
# return self._mgr.external_values()
...
@@ -0,0 +1,5 @@
import pandas as pd
# Series case
a = pd.Series([1, 2, 3])
<warning descr="Method Series.to_list() is recommended">list<caret>(a.values)</warning>
@@ -0,0 +1,5 @@
import pandas as pd
# Series case
a = pd.Series([1, 2, 3])
a.to_list()
@@ -0,0 +1,29 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.quickFixes
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PyQuickFixTestCase
import com.jetbrains.python.inspections.PyPandasSeriesToListInspection
class PyPandasSeriesToListQuickFixTest : PyQuickFixTestCase() {
private val quickFixName = PyPsiBundle.message("QFIX.pandas.series.values.replace.with.tolist")
@Throws(Exception::class)
override fun setUp() {
super.setUp()
myFixture.copyDirectoryToProject("", "")
}
fun testDataframeGetitem() {
doQuickFixTest(PyPandasSeriesToListInspection::class.java, quickFixName)
}
fun testDataframeGetattr() {
doQuickFixTest(PyPandasSeriesToListInspection::class.java, quickFixName)
}
fun testSeriesSimple() {
doQuickFixTest(PyPandasSeriesToListInspection::class.java, quickFixName)
}
}